Skip to content

Commit

Permalink
feat(panel-app): new app
Browse files Browse the repository at this point in the history
  • Loading branch information
MM25Zamanian committed Jun 16, 2024
1 parent 08a24b2 commit 012b590
Show file tree
Hide file tree
Showing 26 changed files with 628 additions and 133 deletions.
4 changes: 2 additions & 2 deletions packages/panel-app/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<html lang="fa-IR" dir="rtl" class="color-scheme-dark bg-background font-vazirmatn">
<html lang="fa-IR" dir="rtl" class="color-scheme-light bg-background font-vazirmatn">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
Expand All @@ -13,7 +13,7 @@
<script type="module" src="/src/main.ts" async></script>
</head>

<body class="bg-surface max-w-[24.5rem] mx-auto px-4 w-full h-full overflow-visible flex flex-col">
<body class="bg-surface max-w-screen-md mx-auto w-full h-full overflow-visible flex flex-col">
<div style="display: flex; width: 100%; height: 100%; align-items: center; justify-content: center">
<svg xmlns="http://www.w3.org/2000/svg" width="96" height="96" viewBox="0 0 24 24">
<circle cx="12" cy="12" r="0" fill="white">
Expand Down
20 changes: 13 additions & 7 deletions packages/panel-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,29 @@
},
"devDependencies": {
"@fontsource/vazirmatn": "^5.0.20",
"@gecut/components": "^2.3.0",
"@gecut/i18n": "^2.0.4",
"@gecut/components": "^2.3.2",
"@gecut/i18n": "^2.0.6",
"@gecut/kartbook-panel-api": "workspace:^",
"@gecut/kartbook-types": "workspace:^",
"@gecut/lit-helper": "^2.1.3",
"@gecut/logger": "^1.4.2",
"@gecut/signal": "^2.1.2",
"@gecut/lit-helper": "^2.1.5",
"@gecut/logger": "^1.4.3",
"@gecut/signal": "^2.2.1",
"@gecut/styles": "^2.3.1",
"@gecut/types": "^2.2.0",
"@gecut/utilities": "^5.2.1",
"@gecut/types": "^2.2.1",
"@gecut/utilities": "^5.3.0",
"@iconify-json/solar": "^1.1.9",
"@tailwindcss/nesting": "^0.0.0-insiders.565cd3e",
"@thepassle/app-tools": "^0.9.12",
"@trpc/client": "^10.45.2",
"@types/node": "^20.13.0",
"autoprefixer": "^10.4.19",
"lit": "^3.1.3",
"postcss": "^8.4.38",
"postcss-import": "^16.1.0",
"tailwindcss": "^3.4.3",
"unplugin-fonts": "^1.1.1",
"unplugin-icons": "^0.19.0",
"urlpattern-polyfill": "^10.0.0",
"vite": "^5.2.12",
"vite-plugin-pwa": "^0.20.0",
"vite-tsconfig-paths": "^4.3.2"
Expand Down
Binary file added packages/panel-app/public/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion packages/panel-app/src/ui/app-index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import {gecutContext} from '@gecut/lit-helper/directives/context.js';
import {html, render} from 'lit/html.js';
import 'unfonts.css';

import {routerContext} from './contexts/router.js';
import './router/index.js';
import './styles/global.css';

document.body.innerHTML = '';

render(html``, document.body);
render(html` ${gecutContext(routerContext, (router) => router.render())} `, document.body);
11 changes: 11 additions & 0 deletions packages/panel-app/src/ui/client/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {createTRPCProxyClient, httpLink} from '@trpc/client';

import type {AppRouter} from '@gecut/kartbook-panel-api/panel-api.js';

export const client = createTRPCProxyClient<AppRouter>({
links: [
httpLink({
url: 'http://localhost:8083',
}),
],
});
44 changes: 44 additions & 0 deletions packages/panel-app/src/ui/components/otp-timer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import {GecutAsyncDirective} from '@gecut/lit-helper/directives/async-directive.js';
import {noChange} from 'lit';
import {directive} from 'lit/directive.js';

import type {PartInfo} from 'lit-html/async-directive.js';

class OtpTimerDirective extends GecutAsyncDirective {
constructor(partInfo: PartInfo) {
super(partInfo, 'otp-timer');
}

protected timer?: number;

override render(): unknown {
if (!this.timer) {
const time = Date.now() + 60 * 1_000 * 2;

this.timer = setInterval(() => {
const now = Date.now();
const diff = time - now;

const seconds = Math.floor((diff / 1000) % 60);
const minutes = Math.floor((diff / (1000 * 60)) % 60);

if (seconds + minutes <= 0) {
clearInterval(this.timer);
}

this.setValue(`${minutes}:${seconds}`);
}, 500);

this.setValue('2:00');
}

return noChange;
}

protected override disconnected(): void {
super.disconnected();
clearInterval(this.timer);
}
}

export const otpTimer = directive(OtpTimerDirective);
121 changes: 121 additions & 0 deletions packages/panel-app/src/ui/components/sign-in.states.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import {icon, gecutButton} from '@gecut/components';
import {html} from 'lit/html.js';

import {otpTimer} from './otp-timer.js';
import {client} from '../client/index.js';
import {signInStatesContext} from '../contexts/sign-in.states.js';
import {userExistsContext} from '../contexts/user.exists.js';
import {userPartialContext} from '../contexts/user.partial.js';

import SolarNotificationUnreadLinesLineDuotone from '~icons/solar/notification-unread-lines-line-duotone';
import SolarSmartphone2LineDuotone from '~icons/solar/smartphone-2-line-duotone';

import type {StateManager} from '@gecut/utilities/state-manager.js';

export const signInFormStates: StateManager<'tel' | 'otp' | 'info'> = {
tel: () => html`
<label class="gecut-input">
${icon({svg: SolarSmartphone2LineDuotone})}
<input
type="tel"
name="tel"
placeholder="شمـاره همراه"
pattern="^[09]{2}[0-9]{9}$"
.value=${userPartialContext.getValue()?.phoneNumber ?? ''}
required
/>
</label>
${gecutButton({
type: 'filled',
label: 'ورود / ثبت نام',
events: {
click: async (event) => {
const target = event.target as HTMLElement;
const userPartial = await userPartialContext.requireValue();
const phoneNumber = userPartial.phoneNumber;
if (phoneNumber) {
target.setAttribute('loading', '');
client.user.has
.query({phoneNumber})
.then(async (userId) => {
const exists = userId != null;
if (exists) {
await client.user.otp.send.mutate({userId});
}
userExistsContext.setValue(exists);
userPartialContext.setValue({
...userPartial,
_id: userId,
});
})
.finally(() => {
target.removeAttribute('loading');
});
}
},
},
})}
`,
otp: () => html`
<span class="text-bodySmall text-outline mx-auto -mb-2">
لطفا کد اعتبارسنجی ارسال شده به موبایل خود را وارد نمایید.
</span>
<label class="gecut-input">
${icon({svg: SolarNotificationUnreadLinesLineDuotone})}
<input type="tel" name="otp" placeholder="کـد اعتبارسنجـــی" pattern="^[0-9]{6}$" maxlength="6" required />
</label>
<div class="flex justify-between">
<span
class="text-bodySmall font-bold text-primary cursor-pointer"
@click=${() => signInStatesContext.setValue('tel')}
>
ویرایش شماره تلفن
</span>
<span class="text-bodySmall text-onSurfaceVariant"> ${otpTimer()} تا ارسال مجدد </span>
</div>
${gecutButton({
type: 'filled',
label: 'ورود / ثبـت نـام',
events: {
click: async (event) => {
const target = event.target as HTMLElement;
const userPartial = await userPartialContext.requireValue();
if (userPartial._id && userPartial.otp?.code) {
target.setAttribute('loading', '');
client.user.otp.verify
.mutate({
userId: userPartial._id,
otpCode: userPartial.otp?.code,
})
.then((token) => {})
.finally(() => target.removeAttribute('loading'));
}
},
},
})}
`,
info: () => html`
<span class="text-bodySmall text-outline mx-auto -mb-2"> اطلاعات خود را جهت تکمیل حساب وارد کنید. </span>
<label class="gecut-input">
${icon({svg: SolarNotificationUnreadLinesLineDuotone})}
<input type="text" name="first-name" placeholder="نام" minlength="4" required />
</label>
<label class="gecut-input">
${icon({svg: SolarNotificationUnreadLinesLineDuotone})}
<input type="text" name="last-name" placeholder="نام خانوادگـی" minlength="4" required />
</label>
${gecutButton({
type: 'filled',
label: 'ثـــبت اطــلاعات',
})}
`,
};
5 changes: 5 additions & 0 deletions packages/panel-app/src/ui/contexts/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {ContextSignal} from '@gecut/signal';

import type {Router} from '@thepassle/app-tools/router.js';

export const routerContext = new ContextSignal<Router>('router', 'AnimationFrame');
5 changes: 5 additions & 0 deletions packages/panel-app/src/ui/contexts/sign-in.states.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {ContextSignal} from '@gecut/signal';

export const signInStatesContext = new ContextSignal<'tel' | 'otp' | 'info'>('sign-in.states', 'AnimationFrame');

signInStatesContext.setValue('tel');
3 changes: 3 additions & 0 deletions packages/panel-app/src/ui/contexts/user.exists.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import {ContextSignal} from '@gecut/signal';

export const userExistsContext = new ContextSignal<boolean>('user.exists');
8 changes: 8 additions & 0 deletions packages/panel-app/src/ui/contexts/user.partial.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import {ContextSignal} from '@gecut/signal';

import type {UserData} from '@gecut/kartbook-types';
import type {PartialDeep} from '@gecut/types';

export const userPartialContext = new ContextSignal<PartialDeep<UserData>>('user.partial');

userPartialContext.setValue({});
5 changes: 5 additions & 0 deletions packages/panel-app/src/ui/pages/404.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {html} from 'lit/html.js';

export function $404Page() {
return html``;
}
5 changes: 5 additions & 0 deletions packages/panel-app/src/ui/pages/cards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {html} from 'lit/html.js';

export function $CardsPage() {
return html``;
}
96 changes: 96 additions & 0 deletions packages/panel-app/src/ui/pages/sign-in.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {gecutIconButton} from '@gecut/components';
import {gecutContext} from '@gecut/lit-helper/directives/context.js';
import {stateManager} from '@gecut/utilities/state-manager.js';
import {html} from 'lit/html.js';

import {signInFormStates} from '../components/sign-in.states.js';
import {signInStatesContext} from '../contexts/sign-in.states.js';
import {userExistsContext} from '../contexts/user.exists.js';
import {userPartialContext} from '../contexts/user.partial.js';

import SolarHelpLineDuotone from '~icons/solar/help-line-duotone';

const formSubmitter = async (event: SubmitEvent) => {
event.preventDefault();

const userPartial = await userPartialContext.requireValue();
const state = await signInStatesContext.requireValue();
const form = (event.currentTarget || event.target) as HTMLFormElement;
const data = new FormData(form);

switch (state) {
case 'otp':
const otp = data.get('otp')?.toString();

if (otp) {
userPartialContext.setValue({
...userPartial,

otp: {code: otp},
});
}

break;
case 'tel':
const phoneNumber = data.get('tel')?.toString();

if (phoneNumber) {
userPartialContext.setValue({
...userPartial,

phoneNumber,
});
}

if (await userExistsContext.requireValue()) {
signInStatesContext.setValue('otp');
}
else {
signInStatesContext.setValue('info');
}
break;
case 'info':
const firstName = data.get('first-name')?.toString();
const lastName = data.get('last-name')?.toString();

if (firstName && lastName) {
userPartialContext.setValue({
...userPartial,

firstName,
lastName,
});
}

signInStatesContext.setValue('otp');

break;
}

return false;
};

export function $SignPage() {
return html`
<div class="px-4 w-full h-full flex flex-1 flex-col max-w-md mx-auto">
<form class="flex-1 flex flex-col justify-center gap-4 min-h-52 *:animate-fadeInSlide" @submit=${formSubmitter}>
<div class="absolute top-4 left-0 [&>*]:m-0 px-4">
${gecutIconButton({
svg: SolarHelpLineDuotone,
type: 'normal',
})}
</div>
<div class="flex w-full items-center justify-center pb-4">
<img src="/icon.png" class="h-32" />
</div>
${gecutContext(signInStatesContext, (state) => stateManager(signInFormStates, state))}
</form>
<div class="flex flex-col items-center justify-end pb-8">
<p class="text-outline text-bodySmall">پلتفرم شمــاره کــارت آنــلایــن، کارت بــوک</p>
</div>
</div>
`;
}
5 changes: 5 additions & 0 deletions packages/panel-app/src/ui/pages/support.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import {html} from 'lit/html.js';

export function $SupportPage() {
return html``;
}
Loading

0 comments on commit 012b590

Please sign in to comment.