feat: web things; loading

pull/715/head
Travis Fischer 2025-06-17 05:42:06 +07:00
rodzic 85ac47783d
commit 33fc9c331c
12 zmienionych plików z 167 dodań i 77 usunięć

Wyświetl plik

@ -30,6 +30,8 @@
"@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-slot": "^1.2.3",
"@radix-ui/react-tooltip": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.7",
"@tanstack/react-form": "^1.12.3", "@tanstack/react-form": "^1.12.3",
"@tanstack/react-query": "^5.80.7",
"@tanstack/react-query-devtools": "^5.80.7",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"ky": "catalog:", "ky": "catalog:",

Wyświetl plik

@ -1,12 +1,20 @@
'use client' 'use client'
import { useQuery } from '@tanstack/react-query'
import { useAuthenticatedAgentic } from '@/components/agentic-provider' import { useAuthenticatedAgentic } from '@/components/agentic-provider'
import { LoadingIndicator } from '@/components/loading-indicator'
export default function AppIndexPage() { export default function AppIndexPage() {
const ctx = useAuthenticatedAgentic() const ctx = useAuthenticatedAgentic()
const { data: projects, isLoading } = useQuery({
// TODO: loading queryKey: ['projects'],
if (!ctx) return null queryFn: () =>
ctx?.api
.listProjects({ populate: ['lastPublishedDeployment'] })
.catch(() => []),
enabled: !!ctx
})
return ( return (
<> <>
@ -15,7 +23,39 @@ export default function AppIndexPage() {
Authenticated Dashboard Authenticated Dashboard
</h1> </h1>
<pre>Auth Session: {JSON.stringify(ctx.api.authSession, null, 2)}</pre> {!ctx || isLoading ? (
<LoadingIndicator />
) : (
<div className='mt-8'>
<h2 className='text-xl font-semibold mb-4'>Your Projects</h2>
{projects?.length === 0 ? (
<p>
No projects found. Create your first project to get started!
</p>
) : (
<div className='grid gap-4'>
{projects?.map((project) => (
<div
key={project.id}
className='p-4 border rounded-lg hover:border-gray-400 transition-colors'
>
<h3 className='font-medium'>{project.name}</h3>
<p className='text-sm text-gray-500'>
{project.identifier}
</p>
{project.lastPublishedDeployment && (
<p className='text-sm text-gray-500 mt-1'>
Last published:{' '}
{project.lastPublishedDeployment.version ||
project.lastPublishedDeployment.hash}
</p>
)}
</div>
))}
</div>
)}
</div>
)}
</section> </section>
</> </>
) )

Wyświetl plik

@ -4,6 +4,7 @@ import { redirect, RedirectType, useSearchParams } from 'next/navigation'
import { useEffect } from 'react' import { useEffect } from 'react'
import { useUnauthenticatedAgentic } from '@/components/agentic-provider' import { useUnauthenticatedAgentic } from '@/components/agentic-provider'
import { LoadingIndicator } from '@/components/loading-indicator'
import { toastError } from '@/lib/notifications' import { toastError } from '@/lib/notifications'
export function SuccessPage({ export function SuccessPage({
@ -41,6 +42,5 @@ export function SuccessPage({
})() })()
}, [code, ctx]) }, [code, ctx])
// TODO: show a loading state return <LoadingIndicator />
return null
} }

Wyświetl plik

@ -5,14 +5,12 @@ import cs from 'clsx'
import { Geist } from 'next/font/google' import { Geist } from 'next/font/google'
import { Toaster } from 'sonner' import { Toaster } from 'sonner'
import { AgenticProvider } from '@/components/agentic-provider'
import { Bootstrap } from '@/components/bootstrap' import { Bootstrap } from '@/components/bootstrap'
import { Footer } from '@/components/footer' import { Footer } from '@/components/footer'
import { Header } from '@/components/header' import { Header } from '@/components/header'
import { PostHogProvider } from '@/components/posthog-provider'
import { ThemeProvider } from '@/components/theme-provider'
import * as config from '@/lib/config' import * as config from '@/lib/config'
import Providers from './providers'
import styles from './styles.module.css' import styles from './styles.module.css'
const geist = Geist({ const geist = Geist({
@ -50,28 +48,20 @@ export default function RootLayout({
return ( return (
<html lang='en' suppressHydrationWarning> <html lang='en' suppressHydrationWarning>
<body className={`${geist.variable} antialiased`}> <body className={`${geist.variable} antialiased`}>
<PostHogProvider> <Providers>
<AgenticProvider> <div className={styles.root}>
<ThemeProvider <Header />
attribute='class'
defaultTheme='dark'
disableTransitionOnChange
>
<div className={styles.root}>
<Header />
<main className={cs(styles.main, 'pt-8 pb-16 px-4 md:px-0')}> <main className={cs(styles.main, 'pt-8 pb-16 px-4 md:px-0')}>
{children} {children}
</main> </main>
<Toaster richColors /> <Toaster richColors />
<Footer /> <Footer />
</div> </div>
</ThemeProvider>
</AgenticProvider>
</PostHogProvider>
<Bootstrap /> <Bootstrap />
</Providers>
</body> </body>
</html> </html>
) )

Wyświetl plik

@ -0,0 +1,30 @@
'use client'
import type React from 'react'
import { QueryClientProvider } from '@tanstack/react-query'
import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
import { AgenticProvider } from '@/components/agentic-provider'
import { PostHogProvider } from '@/components/posthog-provider'
import { ThemeProvider } from '@/components/theme-provider'
import { queryClient } from '@/lib/query-client'
export default function Providers({ children }: { children: React.ReactNode }) {
return (
<PostHogProvider>
<AgenticProvider>
<ThemeProvider
attribute='class'
defaultTheme='dark'
disableTransitionOnChange
>
<QueryClientProvider client={queryClient}>
{children}
<ReactQueryDevtools />
</QueryClientProvider>
</ThemeProvider>
</AgenticProvider>
</PostHogProvider>
)
}

Wyświetl plik

@ -37,16 +37,16 @@ export function AgenticProvider({ children }: { children: ReactNode }) {
api: new AgenticApiClient({ api: new AgenticApiClient({
apiBaseUrl: config.apiBaseUrl, apiBaseUrl: config.apiBaseUrl,
onUpdateAuth: (updatedAuthSession) => { onUpdateAuth: (updatedAuthSession) => {
console.log('onUpdateAuth', updatedAuthSession) // console.log('onUpdateAuth', updatedAuthSession)
if ( if (
!!authSession !== !!updatedAuthSession && !!authSession !== !!updatedAuthSession &&
authSession?.token !== updatedAuthSession?.token authSession?.token !== updatedAuthSession?.token
) { ) {
console.log('setAuthSession', updatedAuthSession) // console.log('setAuthSession', updatedAuthSession)
setAuthSession(updatedAuthSession) setAuthSession(updatedAuthSession)
} else { } else {
console.log('auth session not updated') // console.log('auth session not updated')
} }
} }
}), }),
@ -54,7 +54,7 @@ export function AgenticProvider({ children }: { children: ReactNode }) {
}) })
useEffect(() => { useEffect(() => {
console.log('updating session from localStorage', authSession?.token) // console.log('updating session from localStorage', authSession?.token)
if (agenticContext.current) { if (agenticContext.current) {
if (authSession) { if (authSession) {
agenticContext.current.api.authSession = authSession agenticContext.current.api.authSession = authSession

Wyświetl plik

@ -11,5 +11,6 @@ export function Bootstrap() {
bootstrap() bootstrap()
} }
// Return `null` so we can use this as a react component
return null return null
} }

Wyświetl plik

@ -26,13 +26,6 @@
backdrop-filter: saturate(180%) blur(20px); backdrop-filter: saturate(180%) blur(20px);
} }
/* Workaround for Firefox not supporting backdrop-filter yet */
@-moz-document url-prefix() {
:global(.dark) .header {
background: hsla(203, 8%, 20%, 0.8);
}
}
.headerContent { .headerContent {
align-self: center; align-self: center;
width: 100%; width: 100%;

Wyświetl plik

@ -1,10 +1,11 @@
'use client' 'use client'
import cs from 'clsx' // import { AnimatePresence, motion } from 'motion/react'
import { AnimatePresence, motion } from 'motion/react'
import dynamic from 'next/dynamic' import dynamic from 'next/dynamic'
import { useTheme } from 'next-themes' import { useTheme } from 'next-themes'
import { cn } from '@/lib/utils'
import loadingDark from './loading-dark.json' import loadingDark from './loading-dark.json'
import loadingLight from './loading-light.json' import loadingLight from './loading-light.json'
import styles from './styles.module.css' import styles from './styles.module.css'
@ -13,44 +14,35 @@ const Lottie = dynamic(() => import('react-lottie-player'), {
ssr: false ssr: false
}) })
export function LoadingIndicator({ export function LoadingIndicator({ className }: { className?: string }) {
isLoading = true,
fill = false,
className,
initial,
animate,
exit,
...rest
}: {
isLoading?: boolean
fill?: boolean
className?: string
initial?: any
animate?: any
exit?: any
}) {
const { resolvedTheme } = useTheme() const { resolvedTheme } = useTheme()
return ( return (
<AnimatePresence> <Lottie
{isLoading ? ( play
<motion.div loop
className={cs(styles.loading, fill && styles.fill, className)} animationData={resolvedTheme === 'dark' ? loadingDark : loadingLight}
initial={{ opacity: 1, ...initial }} className={cn(styles.loadingAnimation, className)}
animate={{ opacity: 1, ...animate }} />
exit={{ opacity: 0, ...exit }}
{...rest} // <AnimatePresence>
> // {isLoading ? (
<Lottie // <motion.div
play // className={cn(styles.loading, fill && styles.fill, className)}
loop // transition={{ duration: 10 }}
animationData={ // exit={{ opacity: 0, ...exit }}
resolvedTheme === 'dark' ? loadingDark : loadingLight // {...rest}
} // >
className={styles.loadingAnimation} // <Lottie
/> // play
</motion.div> // loop
) : null} // animationData={
</AnimatePresence> // resolvedTheme === 'dark' ? loadingDark : loadingLight
// }
// className={styles.loadingAnimation}
// />
// </motion.div>
// ) : null}
// </AnimatePresence>
) )
} }

Wyświetl plik

@ -15,6 +15,7 @@
} }
.loadingAnimation { .loadingAnimation {
pointer-events: none;
width: 200px; width: 200px;
height: 200px; height: 200px;
max-width: 50vw; max-width: 50vw;

Wyświetl plik

@ -0,0 +1,3 @@
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient()

Wyświetl plik

@ -536,6 +536,12 @@ importers:
'@tanstack/react-form': '@tanstack/react-form':
specifier: ^1.12.3 specifier: ^1.12.3
version: 1.12.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0) version: 1.12.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@tanstack/react-query':
specifier: ^5.80.7
version: 5.80.7(react@19.1.0)
'@tanstack/react-query-devtools':
specifier: ^5.80.7
version: 5.80.7(@tanstack/react-query@5.80.7(react@19.1.0))(react@19.1.0)
class-variance-authority: class-variance-authority:
specifier: ^0.7.1 specifier: ^0.7.1
version: 0.7.1 version: 0.7.1
@ -3261,6 +3267,12 @@ packages:
'@tanstack/form-core@1.12.3': '@tanstack/form-core@1.12.3':
resolution: {integrity: sha512-H59XYP8Jxg8vT4IYIZa1BHkYiyiZqFcLSD2HpxefHP/vlG06/spCySVe/vGAP7IJgHHSAlEqBhQoy1Mg2ruTRA==} resolution: {integrity: sha512-H59XYP8Jxg8vT4IYIZa1BHkYiyiZqFcLSD2HpxefHP/vlG06/spCySVe/vGAP7IJgHHSAlEqBhQoy1Mg2ruTRA==}
'@tanstack/query-core@5.80.7':
resolution: {integrity: sha512-s09l5zeUKC8q7DCCCIkVSns8zZrK4ZDT6ryEjxNBFi68G4z2EBobBS7rdOY3r6W1WbUDpc1fe5oY+YO/+2UVUg==}
'@tanstack/query-devtools@5.80.0':
resolution: {integrity: sha512-D6gH4asyjaoXrCOt5vG5Og/YSj0D/TxwNQgtLJIgWbhbWCC/emu2E92EFoVHh4ppVWg1qT2gKHvKyQBEFZhCuA==}
'@tanstack/react-form@1.12.3': '@tanstack/react-form@1.12.3':
resolution: {integrity: sha512-IlWLVKizjV+CzMgzaSac61bS4/UeSL2gceGOVIv+gKs2SriDIyylJd8AcqsKE/kNplm1K0NYXIF2Vk/re+JfOg==} resolution: {integrity: sha512-IlWLVKizjV+CzMgzaSac61bS4/UeSL2gceGOVIv+gKs2SriDIyylJd8AcqsKE/kNplm1K0NYXIF2Vk/re+JfOg==}
peerDependencies: peerDependencies:
@ -3273,6 +3285,17 @@ packages:
vinxi: vinxi:
optional: true optional: true
'@tanstack/react-query-devtools@5.80.7':
resolution: {integrity: sha512-7Dz/19fVo0i+jgLVBabV5vfGOlLyN5L1w8w1/ogFhe6ItNNsNA+ZgNTbtiKpbR3CcX2WDRRTInz1uMSmHzTsoQ==}
peerDependencies:
'@tanstack/react-query': ^5.80.7
react: ^18 || ^19
'@tanstack/react-query@5.80.7':
resolution: {integrity: sha512-u2F0VK6+anItoEvB3+rfvTO9GEh2vb00Je05OwlUe/A0lkJBgW1HckiY3f9YZa+jx6IOe4dHPh10dyp9aY3iRQ==}
peerDependencies:
react: ^18 || ^19
'@tanstack/react-store@0.7.1': '@tanstack/react-store@0.7.1':
resolution: {integrity: sha512-qUTEKdId6QPWGiWyKAPf/gkN29scEsz6EUSJ0C3HgLMgaqTAyBsQ2sMCfGVcqb+kkhEXAdjleCgH6LAPD6f2sA==} resolution: {integrity: sha512-qUTEKdId6QPWGiWyKAPf/gkN29scEsz6EUSJ0C3HgLMgaqTAyBsQ2sMCfGVcqb+kkhEXAdjleCgH6LAPD6f2sA==}
peerDependencies: peerDependencies:
@ -9013,6 +9036,10 @@ snapshots:
dependencies: dependencies:
'@tanstack/store': 0.7.1 '@tanstack/store': 0.7.1
'@tanstack/query-core@5.80.7': {}
'@tanstack/query-devtools@5.80.0': {}
'@tanstack/react-form@1.12.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': '@tanstack/react-form@1.12.3(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies: dependencies:
'@tanstack/form-core': 1.12.3 '@tanstack/form-core': 1.12.3
@ -9023,6 +9050,17 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- react-dom - react-dom
'@tanstack/react-query-devtools@5.80.7(@tanstack/react-query@5.80.7(react@19.1.0))(react@19.1.0)':
dependencies:
'@tanstack/query-devtools': 5.80.0
'@tanstack/react-query': 5.80.7(react@19.1.0)
react: 19.1.0
'@tanstack/react-query@5.80.7(react@19.1.0)':
dependencies:
'@tanstack/query-core': 5.80.7
react: 19.1.0
'@tanstack/react-store@0.7.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)': '@tanstack/react-store@0.7.1(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies: dependencies:
'@tanstack/store': 0.7.1 '@tanstack/store': 0.7.1