feat: improve login ui

This commit is contained in:
0xzio
2025-05-16 16:46:13 +08:00
parent a5eab9724f
commit cfbdb1fdcb
24 changed files with 3229 additions and 217 deletions

View File

@@ -10,7 +10,7 @@
"showSpinner": false
},
"Keyboard": {
"resize": "none"
"resize": "ionic"
}
}
}

View File

@@ -10,7 +10,7 @@
"showSpinner": false
},
"Keyboard": {
"resize": "none"
"resize": "ionic"
}
},
"packageClassList": [

View File

@@ -7,6 +7,7 @@ import { Capacitor } from '@capacitor/core'
import { StatusBar, Style } from '@capacitor/status-bar'
import {
IonApp,
IonNav,
IonRouterOutlet,
IonSplitPane,
setupIonicReact,
@@ -41,6 +42,11 @@ import '@ionic/react/css/palettes/dark.class.css'
// import '@ionic/react/css/palettes/dark.system.css'
/* Theme variables */
import './theme/variables.css'
import { useEffect, useRef } from 'react'
import { appEmitter } from '@penx/emitter'
import { useCreationId } from '@penx/hooks/useCreationId'
import { ICreationNode } from '@penx/model-type'
import { PageCreation } from './pages/PageCreation'
async function init() {
const platform = Capacitor.getPlatform()
@@ -113,6 +119,27 @@ init()
setupIonicReact()
const App: React.FC = () => {
const nav = useRef<HTMLIonNavElement>(null)
const { creationId, setCreationId } = useCreationId()
useEffect(() => {
// if (initRef.current) return
// initRef.current = true
function handle(creation: ICreationNode) {
console.log('handle route to creation: ', creation.id)
// setCreationId(creation.id)
nav.current?.push(PageCreation, {
creationId: creation.id,
nav: nav.current,
})
}
appEmitter.on('ROUTE_TO_CREATION', handle)
return () => {
appEmitter.off('ROUTE_TO_CREATION', handle)
}
}, [])
return (
<IonApp>
<LinguiClientProvider initialLocale={'en'} initialMessages={{}}>
@@ -126,7 +153,7 @@ const App: React.FC = () => {
<Redirect to="/folder/area" />
</Route>
<Route path="/folder/:name" exact={true}>
<PageHome />
<IonNav ref={nav} root={() => <PageHome />}></IonNav>
</Route>
</IonRouterOutlet>
</IonSplitPane>

View File

@@ -20,7 +20,7 @@ export const Footer = ({ onAdd }: Props) => {
'--border-width': 0,
}}
>
<div className="bg-background flex items-center justify-between gap-3 rounded-full px-3 dark:bg-neutral-900">
<div className="flex items-center justify-between gap-3 rounded-full px-3">
<Button
size="icon"
variant="ghost"
@@ -36,7 +36,7 @@ export const Footer = ({ onAdd }: Props) => {
variant="ghost"
className={cn('size-8 rounded-full')}
onClick={async () => {
setType('HOME')
setType('NOTE')
}}
>
<span className="icon-[solar--notes-linear] size-6"></span>
@@ -66,7 +66,7 @@ export const Footer = ({ onAdd }: Props) => {
variant="ghost"
className={cn('size-8 rounded-full')}
onClick={async () => {
setType('TASK')
setType('PROFILE')
}}
>
<span className="icon-[solar--user-linear] size-6"></span>

View File

@@ -15,10 +15,8 @@ import { IconGoogle } from '@penx/uikit/IconGoogle'
import { LoadingDots } from '@penx/uikit/loading-dots'
import { LoginForm } from './LoginForm'
interface Props {
setVisible: React.Dispatch<React.SetStateAction<boolean>>
}
export function AppleLoginButton({ setVisible }: Props) {
interface Props {}
export function AppleLoginButton({}: Props) {
const { login } = useSession()
const [json, setJson] = useState({})
const [error, setError] = useState({})
@@ -58,7 +56,6 @@ export function AppleLoginButton({ setVisible }: Props) {
await set('SESSION', session)
queryClient.setQueryData(['SESSION'], session)
appEmitter.emit('APP_LOGIN_SUCCESS', session)
setVisible(false)
} catch (error) {
console.log('=========error:', error)

View File

@@ -0,0 +1,42 @@
'use client'
import React, { useMemo, useState } from 'react'
import { PageEmailLogin } from '@/pages/PageEmailLogin'
import { Capacitor } from '@capacitor/core'
import { SocialLogin } from '@capgo/capacitor-social-login'
import { IonButton, IonNavLink, useIonRouter } from '@ionic/react'
import { set } from 'idb-keyval'
import { MailIcon } from 'lucide-react'
import { appEmitter } from '@penx/emitter'
import { localDB } from '@penx/local-db'
import { queryClient } from '@penx/query-client'
import { useSession } from '@penx/session'
import { MobileGoogleLoginInfo } from '@penx/types'
import { Button } from '@penx/uikit/button'
import { IconGoogle } from '@penx/uikit/IconGoogle'
import { LoadingDots } from '@penx/uikit/loading-dots'
import { LoginForm } from './LoginForm'
interface Props {}
export function EmailLoginButton({}: Props) {
const { login } = useSession()
const [json, setJson] = useState({})
const [error, setError] = useState({})
const [session, setSession] = useState({})
const [loading, setLoading] = useState(false)
const router = useIonRouter()
return (
<IonNavLink routerDirection="forward" component={() => <PageEmailLogin />}>
<Button className="w-full gap-2">
{loading && <LoadingDots className="bg-foreground" />}
{!loading && (
<>
<MailIcon size={20} />
<div className="">Email login</div>
</>
)}
</Button>
</IonNavLink>
)
}

View File

@@ -14,10 +14,8 @@ import { IconGoogle } from '@penx/uikit/IconGoogle'
import { LoadingDots } from '@penx/uikit/loading-dots'
import { LoginForm } from './LoginForm'
interface Props {
setVisible: React.Dispatch<React.SetStateAction<boolean>>
}
export function GoogleLoginButton({ setVisible }: Props) {
interface Props {}
export function GoogleLoginButton({}: Props) {
const { login } = useSession()
const [json, setJson] = useState({})
const [error, setError] = useState({})
@@ -53,7 +51,6 @@ export function GoogleLoginButton({ setVisible }: Props) {
await set('SESSION', session)
queryClient.setQueryData(['SESSION'], session)
appEmitter.emit('APP_LOGIN_SUCCESS', session)
setVisible(false)
} catch (error) {
console.log('=========error:', error)
@@ -64,7 +61,7 @@ export function GoogleLoginButton({ setVisible }: Props) {
setLoading(false)
}
return (
<Button onClick={onLogin} className="w-full gap-2" variant="secondary">
<Button onClick={onLogin} className="w-full gap-2">
{loading && <LoadingDots className="bg-foreground" />}
{!loading && (
<>

View File

@@ -1,45 +0,0 @@
'use client'
import React, { useState } from 'react'
import { IonMenuToggle } from '@ionic/react'
import { Drawer } from 'vaul'
import { useSession } from '@penx/session'
import { Button } from '@penx/uikit/button'
import { DialogDescription, DialogTitle } from '@penx/uikit/dialog'
import { LoginContent } from './LoginContent'
import { ProfileButton } from './ProfileButton'
export function LoginButton() {
const { isLoading, session } = useSession()
const [visible, setVisible] = useState(false)
if (session) {
return <ProfileButton />
}
return (
<>
<IonMenuToggle>
<Button size="sm" onClick={() => setVisible(true)}>
Log in
</Button>
</IonMenuToggle>
<Drawer.Root open={visible} onOpenChange={setVisible}>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
<Drawer.Content className="bg-background text-foreground fixed bottom-0 left-0 right-0 flex max-h-[95vh] min-h-[95vh] flex-col rounded-t-[10px] px-0 pb-0 outline-none">
<div
aria-hidden
className="bg-foreground/30 mx-auto mb-4 mt-2 h-1 w-10 flex-shrink-0 rounded-full"
/>
<DialogTitle className="hidden">
<DialogDescription />
</DialogTitle>
{session && <div>{JSON.stringify(session, null, 2)}</div>}
<LoginContent setVisible={setVisible} />
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
</>
)
}

View File

@@ -13,24 +13,24 @@ import { Button } from '@penx/uikit/button'
import { IconGoogle } from '@penx/uikit/IconGoogle'
import { LoadingDots } from '@penx/uikit/loading-dots'
import { AppleLoginButton } from './AppleLoginButton'
import { EmailLoginButton } from './EmailLoginButton'
import { GoogleLoginButton } from './GoogleLoginButton'
import { LoginForm } from './LoginForm'
interface Props {
setVisible: React.Dispatch<React.SetStateAction<boolean>>
}
interface Props {}
const platform = Capacitor.getPlatform()
export function LoginContent({ setVisible }: Props) {
export function LoginContent({}: Props) {
return (
<div className="-mt-10 flex h-full flex-1 flex-col justify-center px-6">
<div className="flex h-full flex-1 flex-col justify-center px-6">
<div className="space-y-2">
<GoogleLoginButton setVisible={setVisible} />
{platform === 'ios' && <AppleLoginButton setVisible={setVisible} />}
<GoogleLoginButton />
{platform === 'ios' && <AppleLoginButton />}
<div className="text-foreground/40 my-4 text-center">or</div>
<LoginForm setVisible={setVisible} />
<EmailLoginButton />
{/* <div className="text-foreground/40 my-4 text-center">or</div>
<LoginForm setVisible={setVisible} /> */}
</div>
</div>
)

View File

@@ -32,11 +32,9 @@ const FormSchema = z.object({
}),
})
interface Props {
setVisible: React.Dispatch<React.SetStateAction<boolean>>
}
interface Props {}
export function LoginForm({ setVisible }: Props) {
export function LoginForm({}: Props) {
const [isLoading, setLoading] = useState(false)
const { login } = useSession()
@@ -64,7 +62,6 @@ export function LoginForm({ setVisible }: Props) {
if (!session.isLoggedIn) {
toast.error(session.message)
} else {
setVisible(false)
await set('SESSION', session)
queryClient.setQueryData(['SESSION'], session)
appEmitter.emit('APP_LOGIN_SUCCESS', session)

View File

@@ -1,70 +0,0 @@
'use client'
import React, { useState } from 'react'
import { Drawer } from 'vaul'
import { useSession } from '@penx/session'
import { Avatar, AvatarFallback, AvatarImage } from '@penx/uikit/avatar'
import { Button } from '@penx/uikit/button'
import { DialogDescription, DialogTitle } from '@penx/uikit/dialog'
import { cn, getUrl } from '@penx/utils'
import { generateGradient } from '@penx/utils/generateGradient'
import { LoginContent } from './LoginContent'
export function ProfileButton() {
const { session, logout } = useSession()
const [visible, setVisible] = useState(false)
if (!session) return
return (
<>
<Avatar className="size-7" onClick={() => setVisible(true)}>
<AvatarImage src={getUrl(session?.image)} />
<AvatarFallback
className={cn(generateGradient(session?.name))}
></AvatarFallback>
</Avatar>
<Drawer.Root open={visible} onOpenChange={setVisible}>
<Drawer.Portal>
<Drawer.Overlay className="fixed inset-0 bg-black/40" />
<Drawer.Content className="bg-background fixed bottom-0 left-0 right-0 flex max-h-[95vh] min-h-[95vh] flex-col rounded-t-[10px] px-0 pb-0 outline-none text-foreground">
<div
aria-hidden
className="mx-auto mb-4 mt-2 h-1.5 w-12 flex-shrink-0 rounded-full"
/>
<DialogTitle className="hidden">
<DialogDescription />
</DialogTitle>
<div className="flex flex-1 flex-col items-center">
<div className="flex flex-1 flex-col items-center">
<Avatar className="size-12" onClick={() => setVisible(true)}>
<AvatarImage src={getUrl(session?.image)} />
<AvatarFallback
className={cn(generateGradient(session?.name))}
></AvatarFallback>
</Avatar>
<div className="font-semibold">{session.name}</div>
<div className="text-foreground/50 text-sm">
{session.email}
</div>
</div>
<div className="pb-6">
<Button
variant="ghost"
className="mt-4 text-red-500"
onClick={() => {
setVisible(false)
logout()
}}
>
Logout
</Button>
</div>
</div>
</Drawer.Content>
</Drawer.Portal>
</Drawer.Root>
</>
)
}

View File

@@ -1,12 +1,9 @@
import { Capacitor } from '@capacitor/core';
import { IonContent, IonMenu } from '@ionic/react';
import { ProfileButton } from '@penx/components/ProfileButton';
import { useSession } from '@penx/session';
import { cn } from '@penx/utils';
import { AreaList } from './AreaList';
import { LoginButton } from './Login/LoginButton';
import { MobileModeToggle } from './MobileModeToggle';
import { Capacitor } from '@capacitor/core'
import { IonContent, IonMenu } from '@ionic/react'
import { useSession } from '@penx/session'
import { cn } from '@penx/utils'
import { AreaList } from './AreaList'
import { MobileModeToggle } from './MobileModeToggle'
const platform = Capacitor.getPlatform()
@@ -17,7 +14,7 @@ const Menu: React.FC = () => {
return (
<IonMenu contentId="main" type="overlay">
<IonContent className="ion-padding safe-area h-full drawer-menu">
<IonContent className="ion-padding safe-area drawer-menu h-full">
<div
className={cn(
'flex h-full flex-col pt-5',
@@ -29,7 +26,6 @@ const Menu: React.FC = () => {
</div>
<div className="flex items-center justify-between">
<MobileModeToggle />
<ProfileButton loginButton={<LoginButton></LoginButton>} />
</div>
</div>
</IonContent>
@@ -37,4 +33,4 @@ const Menu: React.FC = () => {
)
}
export default Menu
export default Menu

View File

@@ -7,14 +7,9 @@ import { AreaWidgets } from '@penx/components/AreaWidgets'
import { MobileAddCreationButton } from './MobileAddCreationButton'
export function MobileHome() {
const [mode, setMode] = React.useState({})
useEffect(() => {
DarkMode.isDarkMode().then((isDark) => setMode(isDark))
}, [])
return (
<div className="">
{/* <QuickSearchTrigger /> */}
<div>isDark: {JSON.stringify(mode, null, 2)}</div>
<AreaWidgets />
{/* <div className="fixed bottom-3 left-0 flex w-full items-center justify-center">
<MobileAddCreationButton />

View File

@@ -4,52 +4,35 @@ import { MobileCreation } from '@/components/MobileCreation'
import { Capacitor } from '@capacitor/core'
import { OverlayEventDetail } from '@ionic/core'
import {
IonBackButton,
IonButton,
IonButtons,
IonContent,
IonFab,
IonHeader,
IonModal,
IonToolbar,
} from '@ionic/react'
import { XIcon } from 'lucide-react'
import { appEmitter } from '@penx/emitter'
import { useCreationId } from '@penx/hooks/useCreationId'
import { ICreationNode } from '@penx/model-type'
const platform = Capacitor.getPlatform()
export const PageCreation: React.FC = () => {
const modal = useRef<HTMLIonModalElement>(null)
const [creationId, setCreationId] = useState('')
export const PageCreation = ({
creationId,
nav,
}: {
creationId: string
nav: HTMLIonNavElement
}) => {
// const { creationId, setCreationId } = useCreationId()
useEffect(() => {
// if (initRef.current) return
// initRef.current = true
function handle(creation: ICreationNode) {
console.log('handle route to creation: ', creation.id)
setCreationId(creation.id)
modal.current?.present()
}
appEmitter.on('ROUTE_TO_CREATION', handle)
return () => {
appEmitter.off('ROUTE_TO_CREATION', handle)
}
}, [])
function onWillDismiss(event: CustomEvent<OverlayEventDetail>) {
if (event.detail.role === 'confirm') {
// setMessage(`Hello, ${event.detail.data}!`)
}
}
if (!creationId) return null
// if (!creationId) return null
return (
<IonModal
ref={modal}
trigger="open-modal"
onWillDismiss={(event) => onWillDismiss(event)}
>
<>
<IonHeader
className={platform === 'android' ? 'safe-area' : ''}
style={{
@@ -66,16 +49,15 @@ export const PageCreation: React.FC = () => {
}}
>
<IonButtons slot="start">
<IonButton color="dark" onClick={() => modal.current?.dismiss()}>
<XIcon size={20} />
</IonButton>
<IonBackButton color="dark" text=""></IonBackButton>
</IonButtons>
{/* <IonTitle>Welcome</IonTitle> */}
<IonButtons slot="end">
<CreationMenu
creationId={creationId}
afterDelete={() => {
modal.current?.dismiss()
// modal.current?.dismiss()
nav.pop()
}}
/>
</IonButtons>
@@ -83,7 +65,14 @@ export const PageCreation: React.FC = () => {
</IonHeader>
<IonContent className="ion-padding">
<MobileCreation creationId={creationId} />
<IonFab
slot="fixed"
vertical="bottom"
className="flex w-full justify-center"
>
<div className="h-10 bg-amber-100">Hello world</div>
</IonFab>
</IonContent>
</IonModal>
</>
)
}

View File

@@ -0,0 +1,33 @@
import React from 'react'
import {
IonBackButton,
IonButton,
IonButtons,
IonContent,
IonHeader,
IonNavLink,
IonTitle,
IonToolbar,
} from '@ionic/react'
export function PageEmailLogin() {
return (
<>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton></IonBackButton>
</IonButtons>
<IonTitle>Page Two</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent class="ion-padding">
<h1>Page Two</h1>
{/* <IonNavLink routerDirection="forward" component={() => <PageThree />}>
<IonButton>Go to Page Three</IonButton>
</IonNavLink> */}
</IonContent>
</>
)
}

View File

@@ -1,5 +1,6 @@
import React, { useEffect, useRef, useState } from 'react'
import { Footer } from '@/components/Footer'
import { LoginContent } from '@/components/Login/LoginContent'
import { MobileHome } from '@/components/MobileHome'
import { SearchButton } from '@/components/MobileSearch/SearchButton'
import { MobileTask } from '@/components/MobileTask/MobileTask'
@@ -37,6 +38,8 @@ import { AreaDialog } from '@penx/components/AreaDialog'
import { QuickInput } from '@penx/components/QuickInput'
import { appEmitter } from '@penx/emitter'
import { useArea } from '@penx/hooks/useArea'
import { useCreationId } from '@penx/hooks/useCreationId'
import { ICreationNode } from '@penx/model-type'
import { Button } from '@penx/uikit/button'
import { Separator } from '@penx/uikit/separator'
import { cn } from '@penx/utils'
@@ -44,7 +47,7 @@ import { PageCreation } from './PageCreation'
const platform = Capacitor.getPlatform()
const PageHome: React.FC = () => {
const PageHome: React.FC = ({ nav }: any) => {
const [open, setOpen] = useState(false)
const [scrolled, setScrolled] = useState(false)
const { area } = useArea()
@@ -53,6 +56,8 @@ const PageHome: React.FC = () => {
const inputRef = useRef<HTMLTextAreaElement>(null)
const { creationId, setCreationId } = useCreationId()
const handleScroll = (event: CustomEvent) => {
const scrollTop = event.detail.scrollTop
setScrolled(scrollTop > 0)
@@ -185,7 +190,7 @@ const PageHome: React.FC = () => {
</IonToolbar>
</IonHeader>
<IonContent fullscreen className="text-foreground">
<IonContent fullscreen className="text-foreground content">
{/* <IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">{name}</IonTitle>
@@ -193,21 +198,19 @@ const PageHome: React.FC = () => {
</IonHeader> */}
<div
className="text-foreground relative flex flex-col px-1 pb-14"
className="text-foreground relative flex min-h-full flex-col px-1"
style={
{
'--background': 'oklch(1 0 0)',
} as any
}
>
<div className="">
{isHome && <MobileHome />}
{!isHome && <MobileTask />}
</div>
{type === 'HOME' && <MobileHome />}
{type === 'TASK' && <MobileTask />}
{type === 'PROFILE' && <LoginContent />}
</div>
<PageCreation />
</IonContent>
<Footer
onAdd={() => {
setOpen(true)

View File

@@ -15,16 +15,25 @@
--background: white;
}
.content {
--background: white;
}
.dark .content,
.ion-palette-dark .content {
--background: #222;
}
.dark .toolbar,
.ion-palette-dark .toolbar {
/* --background: hsl(240 10% 3.9%); */
--background: dark;
--background: #222;
}
.dark .drawer-menu,
.ion-palette-dark .drawer-menu {
/* --background: hsl(240 10% 3.9%); */
--background: #111;
--background: #333;
}
:root,

View File

@@ -243,6 +243,7 @@ export function Creation({ panel, className }: Props) {
}
showAddButton
showFixedToolbar={false}
// showFixedToolbar
onChange={(v: any[]) => {
const input: UpdateCreationInput = {
id: creation.id,

View File

@@ -27,6 +27,7 @@ import { cursorOverlayPlugin } from './cursor-overlay-plugin'
import { deletePlugins } from './delete-plugins'
import { dndPlugins } from './dnd-plugins'
import { exitBreakPlugin } from './exit-break-plugin'
import { FixedToolbarPlugin } from './fixed-toolbar-plugin'
import { resetBlockTypePlugin } from './reset-block-type-plugin'
import { softBreakPlugin } from './soft-break-plugin'
import { viewPlugins } from './views-plugins'

View File

@@ -1,18 +1,20 @@
'use client'
import { createPlatePlugin } from '@udecode/plate/react'
import { FixedToolbar } from '@penx/editor-plugins/plate-ui/fixed-toolbar'
import { FixedToolbarButtons } from '@penx/editor-plugins/plate-ui/fixed-toolbar-buttons'
import { createPlatePlugin } from '@udecode/plate/react'
export const FixedToolbarPlugin = createPlatePlugin({
key: 'fixed-toolbar',
render: {
beforeEditable: () => (
<div className="sm:px-[max(10px,calc(50%-350px))]">
<FixedToolbar className="bg-background">
<FixedToolbarButtons />
</FixedToolbar>
</div>
),
beforeEditable: () => {
return (
<div className="sm:px-[max(10px,calc(50%-350px))]">
<FixedToolbar className="bg-background">
<FixedToolbarButtons />
</FixedToolbar>
</div>
)
},
},
})

View File

@@ -4,12 +4,16 @@ import React, { useEffect, useImperativeHandle, useRef, useState } from 'react'
import { DndProvider } from 'react-dnd'
import { HTML5Backend } from 'react-dnd-html5-backend'
import isEqual from 'react-fast-compare'
import { ItalicPlugin } from '@udecode/plate-basic-marks/react'
import { Plate } from '@udecode/plate/react'
import { ItalicIcon } from 'lucide-react'
import {
Editor,
EditorContainer,
EditorVariantProps,
} from '@penx/editor-plugins/plate-ui/editor'
import { MarkToolbarButton } from '@penx/editor-plugins/plate-ui/mark-toolbar-button'
import { Toolbar } from '@penx/editor-plugins/plate-ui/toolbar'
import {
PlateEditorType,
useCreateEditor,

3034
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff