mirror of
https://github.com/chinchang/web-maker.git
synced 2025-07-27 00:30:09 +02:00
add subs details in profile modal
This commit is contained in:
@@ -132,6 +132,20 @@ export default class Footer extends Component {
|
|||||||
<use xlinkHref="#twitter-icon" />
|
<use xlinkHref="#twitter-icon" />
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
|
{user?.isPro ? (
|
||||||
|
<Button
|
||||||
|
onClick={this.props.proBtnClickHandler}
|
||||||
|
data-event-category="ui"
|
||||||
|
data-event-action="manageProFooterBtnClick"
|
||||||
|
class="footer__link ml-1 hint--rounded hint--top-right hide-on-mobile support-link"
|
||||||
|
aria-label={i18n._(t`Manage your PRO subscription`)}
|
||||||
|
>
|
||||||
|
<HStack gap={1}>
|
||||||
|
<Trans>Manage</Trans>
|
||||||
|
<ProBadge />
|
||||||
|
</HStack>
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
<Button
|
<Button
|
||||||
onClick={this.props.proBtnClickHandler}
|
onClick={this.props.proBtnClickHandler}
|
||||||
data-event-category="ui"
|
data-event-category="ui"
|
||||||
@@ -146,6 +160,7 @@ export default class Footer extends Component {
|
|||||||
<ProBadge />
|
<ProBadge />
|
||||||
</HStack>
|
</HStack>
|
||||||
</Button>
|
</Button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{this.props.prefs.isJs13kModeOn ? (
|
{this.props.prefs.isJs13kModeOn ? (
|
||||||
|
31
src/components/Panel.jsx
Normal file
31
src/components/Panel.jsx
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { forwardRef } from 'preact/compat';
|
||||||
|
|
||||||
|
export const Panel = forwardRef(function Panel(
|
||||||
|
{
|
||||||
|
classes = '',
|
||||||
|
padding = '2rem',
|
||||||
|
fullWidth = true,
|
||||||
|
fullHeight = false,
|
||||||
|
glowing = false,
|
||||||
|
topFocus = false,
|
||||||
|
onlyBorder = false,
|
||||||
|
children
|
||||||
|
},
|
||||||
|
ref
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={{
|
||||||
|
padding: padding,
|
||||||
|
width: fullWidth ? '100%' : 'auto',
|
||||||
|
height: fullHeight ? '100%' : 'auto'
|
||||||
|
}}
|
||||||
|
className={`panel ${classes} ${glowing && 'panelGlowing'} ${
|
||||||
|
topFocus && 'panelTopFocus'
|
||||||
|
} ${onlyBorder && 'panelOnlyBorder'}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@@ -1,34 +1,36 @@
|
|||||||
import { h } from 'preact';
|
import { useState, useEffect } from 'preact/hooks';
|
||||||
import { ProBadge } from './ProBadge';
|
import { ProBadge } from './ProBadge';
|
||||||
import { HStack, Stack, VStack } from './Stack';
|
import { HStack, Stack, VStack } from './Stack';
|
||||||
|
import { Panel } from './Panel';
|
||||||
|
import { Text } from './Text';
|
||||||
|
import { getHumanReadableDate } from '../utils';
|
||||||
|
|
||||||
const DEFAULT_PROFILE_IMG =
|
const DEFAULT_PROFILE_IMG =
|
||||||
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ccc' d='M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z'/%3E%3C/svg%3E";
|
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23ccc' d='M12,19.2C9.5,19.2 7.29,17.92 6,16C6.03,14 10,12.9 12,12.9C14,12.9 17.97,14 18,16C16.71,17.92 14.5,19.2 12,19.2M12,5A3,3 0 0,1 15,8A3,3 0 0,1 12,11A3,3 0 0,1 9,8A3,3 0 0,1 12,5M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12C22,6.47 17.5,2 12,2Z'/%3E%3C/svg%3E";
|
||||||
|
|
||||||
export function Profile({ user, logoutBtnHandler }) {
|
const Header = ({ user, logoutBtnHandler }) => {
|
||||||
return (
|
return (
|
||||||
<div class="tac">
|
<Stack gap={5}>
|
||||||
|
<Stack gap={2} align="center">
|
||||||
<img
|
<img
|
||||||
height="80"
|
height="80"
|
||||||
class="profile-modal__avatar-img"
|
class={`profile-modal__avatar-img ${user?.isPro ? 'is-pro' : ''}`}
|
||||||
src={user ? user.photoURL || DEFAULT_PROFILE_IMG : ''}
|
src={user ? user.photoURL || DEFAULT_PROFILE_IMG : ''}
|
||||||
id="profileAvatarImg"
|
id="profileAvatarImg"
|
||||||
alt="Profile image"
|
alt="Profile image"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<VStack gap={4}>
|
<VStack gap={1} align="flex-start">
|
||||||
<VStack gap={1}>
|
<h3
|
||||||
<h3 id="profileUserName">
|
class={`profile-modal__name ${user?.isPro ? 's-pro' : ''}`}
|
||||||
|
id="profileUserName"
|
||||||
|
>
|
||||||
{user && user.displayName ? user.displayName : 'Anonymous Creator'}
|
{user && user.displayName ? user.displayName : 'Anonymous Creator'}
|
||||||
</h3>
|
</h3>
|
||||||
{user.isPro && (
|
{user.isPro && <ProBadge />}
|
||||||
<Stack justify="center">
|
|
||||||
<ProBadge />
|
|
||||||
</Stack>
|
|
||||||
)}
|
|
||||||
</VStack>
|
</VStack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<HStack>
|
|
||||||
<button
|
<button
|
||||||
class="btn btn--primary"
|
class="btn btn--primary"
|
||||||
aria-label="Logout from your account"
|
aria-label="Logout from your account"
|
||||||
@@ -36,8 +38,76 @@ export function Profile({ user, logoutBtnHandler }) {
|
|||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
</HStack>
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function Profile({ user, logoutBtnHandler }) {
|
||||||
|
const [currentSubscription, setCurrentSubscription] = useState(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (user?.isPro) {
|
||||||
|
window.db.getUserSubscriptionEvents(user.uid).then(events => {
|
||||||
|
console.log(events);
|
||||||
|
const creationEvent = events
|
||||||
|
.filter(event => event.type === 'subscription_created')
|
||||||
|
.sort((a, b) => b.timestamp.seconds - a.timestamp.seconds)[0];
|
||||||
|
if (creationEvent) {
|
||||||
|
console.log(creationEvent);
|
||||||
|
creationEvent.attributes = creationEvent.data.data.attributes;
|
||||||
|
setCurrentSubscription(creationEvent);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [user]);
|
||||||
|
return (
|
||||||
|
<VStack gap={2}>
|
||||||
|
<Header user={user} logoutBtnHandler={logoutBtnHandler} />
|
||||||
|
{currentSubscription ? (
|
||||||
|
<Panel>
|
||||||
|
<VStack align="stretch" gap={1}>
|
||||||
|
<Text>
|
||||||
|
Plan:
|
||||||
|
<Text weight="700">
|
||||||
|
{' '}
|
||||||
|
{currentSubscription.attributes.product_name}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
<Text>
|
||||||
|
Subscription Status:{' '}
|
||||||
|
<Text weight="700">{currentSubscription.attributes.status}</Text>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
Renews on:{' '}
|
||||||
|
<Text weight="700">
|
||||||
|
{getHumanReadableDate(currentSubscription.attributes.renews_at)}
|
||||||
|
</Text>
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href={currentSubscription.attributes.urls.customer_portal}
|
||||||
|
>
|
||||||
|
Cancel subscription
|
||||||
|
</a>
|
||||||
|
{/* <a
|
||||||
|
target="_blank"
|
||||||
|
href={
|
||||||
|
currentSubscription.attributes.urls
|
||||||
|
.customer_portal_update_subscription
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Link 2
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
target="_blank"
|
||||||
|
href={currentSubscription.attributes.urls.update_payment_method}
|
||||||
|
>
|
||||||
|
Link 3
|
||||||
|
</a> */}
|
||||||
|
</VStack>
|
||||||
|
</Panel>
|
||||||
|
) : null}
|
||||||
</VStack>
|
</VStack>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -32,7 +32,7 @@ const sizes = {
|
|||||||
export const Text = forwardRef(
|
export const Text = forwardRef(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
size = 0,
|
size = 1,
|
||||||
weight = 'normal',
|
weight = 'normal',
|
||||||
tag,
|
tag,
|
||||||
style = 'normal',
|
style = 'normal',
|
||||||
|
@@ -1134,9 +1134,14 @@ export default class App extends Component {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
proBtnClickHandler() {
|
proBtnClickHandler() {
|
||||||
|
if (user?.isPro) {
|
||||||
|
this.setState({ isProfileModalOpen: true });
|
||||||
|
trackEvent('ui', 'manageProBtnClick');
|
||||||
|
} else {
|
||||||
this.setState({ isProModalOpen: true });
|
this.setState({ isProModalOpen: true });
|
||||||
trackEvent('ui', 'proBtnClick');
|
trackEvent('ui', 'proBtnClick');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
codepenBtnClickHandler(e) {
|
codepenBtnClickHandler(e) {
|
||||||
if (this.state.currentItem.cssMode === CssModes.ACSS) {
|
if (this.state.currentItem.cssMode === CssModes.ACSS) {
|
||||||
alert(
|
alert(
|
||||||
|
28
src/db.js
28
src/db.js
@@ -5,6 +5,24 @@ import { deferred } from './deferred';
|
|||||||
import { trackEvent } from './analytics';
|
import { trackEvent } from './analytics';
|
||||||
import { log } from './utils';
|
import { log } from './utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a firestore query snapshot into native array
|
||||||
|
* @param {snapshot} querySnapshot Snapshot object returned by a firestore query
|
||||||
|
*/
|
||||||
|
function getArrayFromQuerySnapshot(querySnapshot) {
|
||||||
|
const arr = [];
|
||||||
|
querySnapshot.forEach(doc => {
|
||||||
|
// doc.data() has to be after doc.id because docs can have `id` key in them which
|
||||||
|
// should override the explicit `id` being set
|
||||||
|
arr.push({
|
||||||
|
id: doc.id,
|
||||||
|
...doc.data()
|
||||||
|
});
|
||||||
|
// documentCache[doc.id] = doc.data()
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
(() => {
|
(() => {
|
||||||
const FAUX_DELAY = 1;
|
const FAUX_DELAY = 1;
|
||||||
|
|
||||||
@@ -179,6 +197,15 @@ import { log } from './utils';
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getUserSubscriptionEvents(userId) {
|
||||||
|
const remoteDb = await getDb();
|
||||||
|
return remoteDb
|
||||||
|
.collection('subscriptions')
|
||||||
|
.where('userId', '==', userId)
|
||||||
|
.get()
|
||||||
|
.then(getArrayFromQuerySnapshot);
|
||||||
|
}
|
||||||
|
|
||||||
window.db = {
|
window.db = {
|
||||||
getDb,
|
getDb,
|
||||||
getUser,
|
getUser,
|
||||||
@@ -187,6 +214,7 @@ import { log } from './utils';
|
|||||||
getSettings,
|
getSettings,
|
||||||
fetchItem,
|
fetchItem,
|
||||||
getPublicItemCount,
|
getPublicItemCount,
|
||||||
|
getUserSubscriptionEvents,
|
||||||
local: dbLocalAlias,
|
local: dbLocalAlias,
|
||||||
sync: dbSyncAlias
|
sync: dbSyncAlias
|
||||||
};
|
};
|
||||||
|
@@ -2223,6 +2223,60 @@ while the theme CSS file is loading */
|
|||||||
|
|
||||||
box-shadow: var(--shadow-elevation-low);
|
box-shadow: var(--shadow-elevation-low);
|
||||||
}
|
}
|
||||||
|
.profile-modal__avatar-img.is-pro {
|
||||||
|
background: linear-gradient(45deg, var(--color-pro-1), var(--color-pro-2));
|
||||||
|
padding: 0.2rem;
|
||||||
|
animation: avatar-rotate 2s forwards;
|
||||||
|
}
|
||||||
|
@keyframes avatar-rotate {
|
||||||
|
0% {
|
||||||
|
transform: rotate(10deg);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.profile-modal__name.is-pro {
|
||||||
|
background: linear-gradient(45deg, var(--color-pro-1), var(--color-pro-2));
|
||||||
|
color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* .PANEL */
|
||||||
|
.panel {
|
||||||
|
--panel-bg: rgb(255 255 255 / 5%);
|
||||||
|
position: relative;
|
||||||
|
background: var(--panel-bg);
|
||||||
|
box-shadow: var(--panel-shadow);
|
||||||
|
border-radius: 1rem;
|
||||||
|
/* backdrop-filter: blur(20px); */
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelOnlyBorder {
|
||||||
|
background: none;
|
||||||
|
box-shadow: none;
|
||||||
|
backdrop-filter: none;
|
||||||
|
border: 2px solid var(--clr-border-1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelGlowing {
|
||||||
|
box-shadow: var(--glow-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panelTopFocus {
|
||||||
|
background: radial-gradient(
|
||||||
|
82.25% 100% at 50% 0%,
|
||||||
|
rgba(var(--rgb-gray-1), 0.75) 37.28%,
|
||||||
|
rgba(var(--rgb-gray-0), 0) 100%
|
||||||
|
);
|
||||||
|
|
||||||
|
box-shadow:
|
||||||
|
0 0 30px rgba(var(--rgb-brand), 0),
|
||||||
|
0px 20px 50px rgba(0, 0, 0, 0.1),
|
||||||
|
inset 0px 1px 3px rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 600px) {
|
@media screen and (max-width: 600px) {
|
||||||
body {
|
body {
|
||||||
font-size: 70%;
|
font-size: 70%;
|
||||||
|
35
src/utils.js
35
src/utils.js
@@ -205,6 +205,41 @@ export function getHumanDate(timestamp) {
|
|||||||
return retVal;
|
return retVal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert any date-ish string/obj to human readable form -> Jul 02, 2021
|
||||||
|
* @param {string?object} date date to be formatted
|
||||||
|
* @returns string
|
||||||
|
*/
|
||||||
|
export function getHumanReadableDate(
|
||||||
|
date,
|
||||||
|
{ showTime = true, utc = false } = {}
|
||||||
|
) {
|
||||||
|
if (!date) return '';
|
||||||
|
let d = typeof date.toDate === 'function' ? date.toDate() : new Date(date);
|
||||||
|
if (utc) {
|
||||||
|
d = new Date(
|
||||||
|
Date.UTC(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let options = {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
};
|
||||||
|
if (showTime) {
|
||||||
|
options = {
|
||||||
|
...options,
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const dateTimeString = d.toLocaleString(false, options);
|
||||||
|
return dateTimeString;
|
||||||
|
}
|
||||||
|
|
||||||
// create a one-time event
|
// create a one-time event
|
||||||
export function once(node, type, callback) {
|
export function once(node, type, callback) {
|
||||||
// create event
|
// create event
|
||||||
|
Reference in New Issue
Block a user