Browse Source

ui(web): dark mode support

pull/28/head
isra el 1 year ago
parent
commit
6b2fbb574a
  1. 2
      web/app/(app)/(auth)/(components)/request-password-reset-form.tsx
  2. 2
      web/app/(app)/(auth)/(components)/reset-password-form.tsx
  3. 4
      web/app/(app)/(auth)/login/page.tsx
  4. 12
      web/app/(app)/(auth)/register/page.tsx
  5. 4
      web/app/(app)/dashboard/(components)/generate-api-key.tsx
  6. 21
      web/app/(app)/layout-wrapper.tsx
  7. 2
      web/app/(app)/layout.tsx
  8. 15
      web/app/(landing-page)/(components)/code-snippet-section.tsx
  9. 2
      web/app/(landing-page)/(components)/customization-section.tsx
  10. 4
      web/app/(landing-page)/(components)/download-app-section.tsx
  11. 2
      web/app/(landing-page)/(components)/features-section.tsx
  12. 4
      web/app/(landing-page)/(components)/hero-section.tsx
  13. 2
      web/app/(landing-page)/(components)/how-it-works-section.tsx
  14. 68
      web/app/(landing-page)/(components)/landing-page-header.tsx
  15. 6
      web/app/(landing-page)/(components)/support-project-section.tsx
  16. 41
      web/app/layout.tsx
  17. 10
      web/components/shared/app-header.tsx
  18. 2
      web/components/shared/customer-support.tsx
  19. 2
      web/components/shared/footer.tsx
  20. 41
      web/components/shared/theme-toggle.tsx
  21. 8
      web/middleware.ts
  22. 1
      web/package.json
  23. 14
      web/pnpm-lock.yaml
  24. 34
      web/styles/main.css

2
web/app/(app)/(auth)/(components)/request-password-reset-form.tsx

@ -58,7 +58,7 @@ export default function RequestPasswordResetForm() {
} }
return ( return (
<div className='flex items-center justify-center min-h-screen bg-gray-100'>
<div className='flex items-center justify-center min-h-screen bg-gray-100 dark:bg-muted'>
<Card className='w-[400px] shadow-lg'> <Card className='w-[400px] shadow-lg'>
<CardHeader className='space-y-1'> <CardHeader className='space-y-1'>
<CardTitle className='text-2xl font-bold text-center'> <CardTitle className='text-2xl font-bold text-center'>

2
web/app/(app)/(auth)/(components)/reset-password-form.tsx

@ -81,7 +81,7 @@ export default function ResetPasswordForm({
} }
return ( return (
<div className='flex items-center justify-center min-h-screen bg-gray-100'>
<div className='flex items-center justify-center min-h-screen bg-gray-100 dark:bg-muted'>
<Card className='w-[400px] shadow-lg'> <Card className='w-[400px] shadow-lg'>
<CardHeader className='space-y-1'> <CardHeader className='space-y-1'>
<CardTitle className='text-2xl font-bold text-center'> <CardTitle className='text-2xl font-bold text-center'>

4
web/app/(app)/(auth)/login/page.tsx

@ -17,7 +17,7 @@ import { Routes } from '@/config/routes'
export default function LoginPage() { export default function LoginPage() {
return ( return (
<div className='flex items-center justify-center min-h-screen bg-gray-100'>
<div className='flex items-center justify-center min-h-screen bg-gray-100 dark:bg-muted'>
<Card className='w-[400px] shadow-lg'> <Card className='w-[400px] shadow-lg'>
<CardHeader className='space-y-1'> <CardHeader className='space-y-1'>
<CardTitle className='text-2xl font-bold text-center'> <CardTitle className='text-2xl font-bold text-center'>
@ -34,7 +34,7 @@ export default function LoginPage() {
<span className='w-full border-t' /> <span className='w-full border-t' />
</div> </div>
<div className='relative flex justify-center text-xs uppercase'> <div className='relative flex justify-center text-xs uppercase'>
<span className='bg-background px-2 text-muted-foreground'>
<span className='bg-background dark:bg-muted px-2 text-muted-foreground'>
Or Or
</span> </span>
</div> </div>

12
web/app/(app)/(auth)/register/page.tsx

@ -16,7 +16,7 @@ import { Routes } from '@/config/routes'
export default function RegisterPage() { export default function RegisterPage() {
return ( return (
<div className='flex items-center justify-center min-h-screen bg-gray-100'>
<div className='flex items-center justify-center min-h-screen bg-gray-100 dark:bg-muted'>
<Card className='w-[450px] shadow-lg'> <Card className='w-[450px] shadow-lg'>
<CardHeader className='space-y-1'> <CardHeader className='space-y-1'>
<CardTitle className='text-2xl font-bold text-center'> <CardTitle className='text-2xl font-bold text-center'>
@ -28,6 +28,16 @@ export default function RegisterPage() {
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<RegisterForm /> <RegisterForm />
<div className='relative mt-4'>
<div className='absolute inset-0 flex items-center'>
<span className='w-full border-t' />
</div>
<div className='relative flex justify-center text-xs uppercase'>
<span className='bg-background dark:bg-muted px-2 text-muted-foreground'>
Or
</span>
</div>
</div>
<div className='mt-4 flex justify-center'> <div className='mt-4 flex justify-center'>
<LoginWithGoogle /> <LoginWithGoogle />
</div> </div>

4
web/app/(app)/dashboard/(components)/generate-api-key.tsx

@ -87,7 +87,7 @@ export default function GenerateApiKey() {
> >
{isGeneratingApiKey ? ( {isGeneratingApiKey ? (
<div className='flex justify-center items-center h-full'> <div className='flex justify-center items-center h-full'>
<Spinner size='sm' className='text-white' />
<Spinner size='sm' className='text-white dark:text-black' />
</div> </div>
) : ( ) : (
'Generate API Key' 'Generate API Key'
@ -111,7 +111,7 @@ export default function GenerateApiKey() {
</DialogHeader> </DialogHeader>
<div className='space-y-6'> <div className='space-y-6'>
<div className='flex justify-center p-4 bg-muted rounded-lg '>
<div className='flex justify-center p-4 bg-muted dark:bg-white rounded-lg '>
{generatedApiKey?.data && ( {generatedApiKey?.data && (
<QRCode value={generatedApiKey?.data} size={120} /> <QRCode value={generatedApiKey?.data} size={120} />
)} )}

21
web/app/(app)/layout-wrapper.tsx

@ -8,6 +8,7 @@ import { ApiEndpoints } from '@/config/api'
import { useEffect } from 'react' import { useEffect } from 'react'
import { usePathname, useRouter } from 'next/navigation' import { usePathname, useRouter } from 'next/navigation'
import { Routes } from '@/config/routes' import { Routes } from '@/config/routes'
import { ThemeProvider } from 'next-themes'
export default function LayoutWrapper({ session, children }) { export default function LayoutWrapper({ session, children }) {
const router = useRouter() const router = useRouter()
@ -35,15 +36,17 @@ export default function LayoutWrapper({ session, children }) {
return ( return (
<> <>
<SessionProvider session={session}>
<QueryClientProvider client={queryClient}>
<GoogleOAuthProvider
clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}
>
{children}
</GoogleOAuthProvider>
</QueryClientProvider>
</SessionProvider>
<ThemeProvider attribute='class' defaultTheme='system' enableSystem>
<SessionProvider session={session}>
<QueryClientProvider client={queryClient}>
<GoogleOAuthProvider
clientId={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}
>
{children}
</GoogleOAuthProvider>
</QueryClientProvider>
</SessionProvider>
</ThemeProvider>
</> </>
) )
} }

2
web/app/(app)/layout.tsx

@ -12,7 +12,7 @@ export default async function RootLayout({ children }: PropsWithChildren) {
<> <>
<LayoutWrapper session={session}> <LayoutWrapper session={session}>
<AppHeader /> <AppHeader />
{children}
<main className='min-h-[80vh]'>{children}</main>
</LayoutWrapper> </LayoutWrapper>
</> </>
) )

15
web/app/(landing-page)/(components)/code-snippet-section.tsx

@ -1,5 +1,13 @@
import { Tabs, TabsContent, TabsList, TabsTrigger } from '../../../components/ui/tabs'
'use client'
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from '../../../components/ui/tabs'
import SyntaxHighlighter from 'react-syntax-highlighter' import SyntaxHighlighter from 'react-syntax-highlighter'
import { dark } from 'react-syntax-highlighter/dist/esm/styles/prism'
const codeSnippets = [ const codeSnippets = [
{ {
@ -56,10 +64,10 @@ print(response.json())`,
export default function CodeSnippetSection() { export default function CodeSnippetSection() {
return ( return (
<section className='container mx-auto py-24 px-4 sm:px-6 lg:px-8 max-w-7xl bg-gray-50'>
<section className='container mx-auto py-24 px-4 sm:px-6 lg:px-8 max-w-7xl bg-gray-50 dark:bg-muted rounded-2xl my-12'>
<div className='mx-auto max-w-[58rem]'> <div className='mx-auto max-w-[58rem]'>
<h3 className='text-3xl font-bold mb-8'>Code Snippet</h3> <h3 className='text-3xl font-bold mb-8'>Code Snippet</h3>
<div className='bg-white p-6 rounded-xl shadow-sm'>
<div className='bg-white dark:bg-black p-6 rounded-xl shadow-sm'>
<Tabs defaultValue={codeSnippets[0].tech} className='w-full'> <Tabs defaultValue={codeSnippets[0].tech} className='w-full'>
<TabsList className='grid w-full grid-cols-3'> <TabsList className='grid w-full grid-cols-3'>
{codeSnippets.map((snippet) => { {codeSnippets.map((snippet) => {
@ -76,6 +84,7 @@ export default function CodeSnippetSection() {
<SyntaxHighlighter <SyntaxHighlighter
language={snippet.language} language={snippet.language}
showLineNumbers={snippet.language !== 'bash'} showLineNumbers={snippet.language !== 'bash'}
style={dark}
// className='min-h-[200px]' // className='min-h-[200px]'
> >
{snippet.snippet} {snippet.snippet}

2
web/app/(landing-page)/(components)/customization-section.tsx

@ -4,7 +4,7 @@ import Link from 'next/link'
export default function CustomizationSection() { export default function CustomizationSection() {
return ( return (
<section className='py-24 bg-gradient-to-b from-blue-50 to-white'>
<section className='py-24 bg-gradient-to-b from-blue-50 to-white dark:from-blue-950 dark:to-muted'>
<div className='container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl'> <div className='container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl'>
<div className='mx-auto max-w-3xl text-center mb-12'> <div className='mx-auto max-w-3xl text-center mb-12'>
<h2 className='text-4xl font-bold mb-4 text-blue-600'> <h2 className='text-4xl font-bold mb-4 text-blue-600'>

4
web/app/(landing-page)/(components)/download-app-section.tsx

@ -7,7 +7,7 @@ export default function DownloadAppSection() {
return ( return (
<section className='container mx-auto py-24 px-4 sm:px-6 lg:px-8 max-w-7xl'> <section className='container mx-auto py-24 px-4 sm:px-6 lg:px-8 max-w-7xl'>
<div className='mx-auto max-w-[58rem] text-center'> <div className='mx-auto max-w-[58rem] text-center'>
<div className='rounded-xl bg-gradient-to-r from-blue-50 to-indigo-50 p-8'>
<div className='rounded-2xl bg-gradient-to-r from-blue-50 to-indigo-50 p-8 dark:from-blue-950 dark:to-muted'>
<div className='mx-auto max-w-sm'> <div className='mx-auto max-w-sm'>
<Image <Image
alt='App preview' alt='App preview'
@ -24,7 +24,7 @@ export default function DownloadAppSection() {
Gateway. Gateway.
</p> </p>
<Link href={Routes.downloadAndroidApp} prefetch={false}> <Link href={Routes.downloadAndroidApp} prefetch={false}>
<Button className='bg-blue-500 hover:bg-blue-600'>
<Button className='bg-blue-500 hover:bg-blue-600 text-white'>
Download App Download App
</Button> </Button>
</Link> </Link>

2
web/app/(landing-page)/(components)/features-section.tsx

@ -17,7 +17,7 @@ export default function FeaturesSection() {
<Card className='flex flex-col items-center justify-center p-6 text-center'> <Card className='flex flex-col items-center justify-center p-6 text-center'>
<Send className='h-12 w-12 mb-4 text-blue-500' /> <Send className='h-12 w-12 mb-4 text-blue-500' />
<h3 className='font-bold'>Send SMS</h3> <h3 className='font-bold'>Send SMS</h3>
<p className='text-sm text-gray-500'>
<p className='text-sm '>
Send SMS to any number from your dashboard or via REST API Send SMS to any number from your dashboard or via REST API
</p> </p>
</Card> </Card>

4
web/app/(landing-page)/(components)/hero-section.tsx

@ -6,7 +6,7 @@ import Link from 'next/link'
export default function HeroSection() { export default function HeroSection() {
return ( return (
<section className='relative overflow-hidden bg-gradient-to-b from-blue-50 to-white py-16 sm:py-24'>
<section className='relative overflow-hidden bg-gradient-to-b from-blue-50 to-white dark:from-blue-950 dark:to-muted py-16 sm:py-24'>
<div className='absolute inset-0 bg-[url(/grid.svg)] bg-center [mask-image:linear-gradient(180deg,white,rgba(255,255,255,0))]'></div> <div className='absolute inset-0 bg-[url(/grid.svg)] bg-center [mask-image:linear-gradient(180deg,white,rgba(255,255,255,0))]'></div>
<div className='container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl relative'> <div className='container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl relative'>
<div className='grid gap-8 lg:grid-cols-2 lg:gap-16'> <div className='grid gap-8 lg:grid-cols-2 lg:gap-16'>
@ -26,7 +26,7 @@ export default function HeroSection() {
</div> </div>
<div className='flex flex-cdol gap-4 flex-row'> <div className='flex flex-cdol gap-4 flex-row'>
<Link href={Routes.register} prefetch={false}> <Link href={Routes.register} prefetch={false}>
<Button className='bg-blue-500 hover:bg-blue-600' size='lg'>
<Button className='bg-blue-500 hover:bg-blue-600 dark:text-white' size='lg'>
Get Started Get Started
</Button> </Button>
</Link> </Link>

2
web/app/(landing-page)/(components)/how-it-works-section.tsx

@ -10,7 +10,7 @@ export default function HowItWorksSection() {
return ( return (
<section <section
id='how-it-works' id='how-it-works'
className='container mx-auto py-24 px-4 sm:px-6 lg:px-8 max-w-7xl bg-gray-50'
className='container mx-auto py-24 px-4 sm:px-6 lg:px-8 max-w-7xl bg-gray-50 dark:bg-muted rounded-2xl'
> >
<div className='mx-auto max-w-[58rem]'> <div className='mx-auto max-w-[58rem]'>
<h2 className='text-3xl font-bold text-center mb-8'>How It Works</h2> <h2 className='text-3xl font-bold text-center mb-8'>How It Works</h2>

68
web/app/(landing-page)/(components)/landing-page-header.tsx

@ -3,48 +3,54 @@ import { MessageSquarePlus, Moon } from 'lucide-react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ExternalLinks } from '@/config/external-links' import { ExternalLinks } from '@/config/external-links'
import { Routes } from '@/config/routes' import { Routes } from '@/config/routes'
import { ThemeProvider } from 'next-themes'
import ThemeToggle from '@/components/shared/theme-toggle'
export default function LandingPageHeader() { export default function LandingPageHeader() {
return ( return (
<header className='sticky top-0 z-50 w-full border-b bg-white/95 backdrop-blur supports-[backdrop-filter]:bg-white/60'>
<div className='container flex h-14 items-center justify-between px-2'>
<Link
className='flex items-center space-x-2'
href={Routes.landingPage}
>
<MessageSquarePlus className='h-6 w-6 text-blue-500' />
<span className='font-bold'>
Text<span className='text-blue-500'>Bee</span>
</span>
</Link>
<nav className='flex items-center space-x-4'>
{/* <Button variant='ghost' size='icon'>
<Moon className='h-4 w-4' />
<span className='sr-only'>Toggle theme</span>
</Button> */}
<ThemeProvider attribute='class' defaultTheme='system'>
<header className='sticky top-0 z-50 w-full border-b bg-white/95 dark:bg-[#1A2752] backdrop-blur supports-[backdrop-filter]:bg-white/60 dark:bg-muted/95'>
<div className='container flex h-14 items-center justify-between px-2'>
<Link <Link
className='text-sm font-medium hover:text-blue-500'
href={ExternalLinks.github}
className='flex items-center space-x-2'
href={Routes.landingPage}
> >
Github
<MessageSquarePlus className='h-6 w-6 text-blue-500' />
<span className='font-bold'>
Text<span className='text-blue-500'>Bee</span>
</span>
</Link> </Link>
<nav className='flex items-center space-x-4'>
<ThemeToggle />
<Link
className='text-sm font-medium hover:text-blue-500'
href={Routes.dashboard}
>
<Button className='bg-blue-500 hover:bg-blue-600 rounded-full'>
Go to Dashboard
</Button>
</Link>
{/* <Link
{/* <Button variant='ghost' size='icon'>
<Moon className='h-4 w-4' />
<span className='sr-only'>Toggle theme</span>
</Button> */}
<Link
className='text-sm font-medium hover:text-blue-500'
href={ExternalLinks.github}
>
Github
</Link>
<Link
className='text-sm font-medium hover:text-blue-500'
href={Routes.dashboard}
>
<Button className='bg-blue-500 hover:bg-blue-600 dark:text-white rounded-full'>
Go to Dashboard
</Button>
</Link>
{/* <Link
className='text-sm font-medium hover:text-blue-500' className='text-sm font-medium hover:text-blue-500'
href='/register' href='/register'
> >
Register Register
</Link> */} </Link> */}
</nav>
</div>
</header>
</nav>
</div>
</header>
</ThemeProvider>
) )
} }

6
web/app/(landing-page)/(components)/support-project-section.tsx

@ -52,7 +52,7 @@ export default function SupportProjectSection() {
return ( return (
<> <>
<section className='container mx-auto py-24 px-4 sm:px-6 lg:px-8 max-w-7xl bg-gray-50'>
<section className='container mx-auto py-24 px-4 sm:px-6 lg:px-8 max-w-7xl bg-gray-50 dark:bg-muted rounded-2xl my-12'>
<div className='mx-auto max-w-[58rem] text-center'> <div className='mx-auto max-w-[58rem] text-center'>
<h2 className='text-3xl font-bold mb-4'>Support The Project</h2> <h2 className='text-3xl font-bold mb-4'>Support The Project</h2>
<p className='text-gray-500 mb-8'> <p className='text-gray-500 mb-8'>
@ -64,7 +64,7 @@ export default function SupportProjectSection() {
</p> </p>
<div className='flex flex-col sm:flex-row justify-center gap-4'> <div className='flex flex-col sm:flex-row justify-center gap-4'>
<Link href={ExternalLinks.patreon} prefetch={false} target='_blank'> <Link href={ExternalLinks.patreon} prefetch={false} target='_blank'>
<Button className='bg-blue-500 hover:bg-blue-600'>
<Button className='bg-blue-500 hover:bg-blue-600 text-white'>
<Heart className='mr-2 h-4 w-4' /> Become a Patron <Heart className='mr-2 h-4 w-4' /> Become a Patron
</Button> </Button>
</Link> </Link>
@ -84,7 +84,7 @@ export default function SupportProjectSection() {
{cryptoWallets.map((wallet, index) => ( {cryptoWallets.map((wallet, index) => (
<div <div
key={index} key={index}
className='flex items-center justify-between p-4 rounded-lg bg-gray-100'
className='flex items-center justify-between p-4 rounded-lg bg-gray-100 dark:bg-muted'
> >
<div> <div>
<h4 className='font-semibold'>{wallet.name}</h4> <h4 className='font-semibold'>{wallet.name}</h4>

41
web/app/layout.tsx

@ -9,6 +9,9 @@ import { Session } from 'next-auth'
import { getServerSession } from 'next-auth' import { getServerSession } from 'next-auth'
import { headers } from 'next/dist/client/components/headers' import { headers } from 'next/dist/client/components/headers'
import { authOptions } from '@/lib/auth' import { authOptions } from '@/lib/auth'
import { PrismaClient } from '@prisma/client'
import prismaClient from '@/lib/prismaClient'
import { userAgent } from 'next/server'
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'textbee.dev - Free and Open-Source SMS Gateway', title: 'textbee.dev - Free and Open-Source SMS Gateway',
@ -59,8 +62,46 @@ export const metadata: Metadata = {
metadataBase: new URL('https://textbee.dev'), metadataBase: new URL('https://textbee.dev'),
} }
const trackPageView = async ({
headerList,
session,
}: {
headerList: Headers
session: Session | null
}) => {
const { ua } = userAgent({
headers: headerList,
})
const url = headerList.get('x-current-url')
const ip = headerList.get('x-forwarded-for')
const referer = headerList.get('referer')
const res = await prismaClient.pageView.create({
data: {
url,
// @ts-ignore
user: session?.user?.id,
userAgent: ua,
ip,
referer,
},
})
return res
}
export default async function RootLayout({ children }: PropsWithChildren) { export default async function RootLayout({ children }: PropsWithChildren) {
const session: Session | null = await getServerSession(authOptions as any) const session: Session | null = await getServerSession(authOptions as any)
const headerList = headers()
trackPageView({ headerList, session })
.catch(console.error)
.then((res) => {
// console.log(res)
})
return ( return (
<html lang='en'> <html lang='en'>
<body> <body>

10
web/components/shared/app-header.tsx

@ -16,6 +16,7 @@ import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet'
import { Menu, LogOut, LayoutDashboard, MessageSquarePlus } from 'lucide-react' import { Menu, LogOut, LayoutDashboard, MessageSquarePlus } from 'lucide-react'
import { signOut, useSession } from 'next-auth/react' import { signOut, useSession } from 'next-auth/react'
import { Routes } from '@/config/routes' import { Routes } from '@/config/routes'
import ThemeToggle from './theme-toggle'
export default function AppHeader() { export default function AppHeader() {
const session = useSession() const session = useSession()
@ -44,7 +45,9 @@ export default function AppHeader() {
{session.data?.user?.name?.charAt(0)} {session.data?.user?.name?.charAt(0)}
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className='hidden md:block'>{session.data?.user?.name}</div>
<div className='hidden md:block'>
{session.data?.user?.name?.split(' ')[0]}
</div>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className='w-56' align='end' forceMount> <DropdownMenuContent className='w-56' align='end' forceMount>
@ -122,7 +125,7 @@ export default function AppHeader() {
<Button <Button
asChild asChild
color='primary' color='primary'
className='bg-blue-500 hover:bg-blue-600 rounded-full'
className='bg-blue-500 hover:bg-blue-600 text-white rounded-full'
> >
<Link href={Routes.register}>Get started</Link> <Link href={Routes.register}>Get started</Link>
</Button> </Button>
@ -148,7 +151,8 @@ export default function AppHeader() {
</Link> </Link>
</div> </div>
<div className='flex flex-1 items-center justify-end space-x-2'> <div className='flex flex-1 items-center justify-end space-x-2'>
<nav className='flex items-center space-x-2'>
<nav className='flex items-center space-x-6'>
<ThemeToggle />
{isAuthenticated ? ( {isAuthenticated ? (
<AuthenticatedMenu /> <AuthenticatedMenu />
) : ( ) : (

2
web/components/shared/customer-support.tsx

@ -88,7 +88,7 @@ export default function SupportButton() {
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button <Button
className='fixed bottom-4 right-4 shadow-lg bg-blue-500 hover:bg-blue-600 rounded-full'
className='fixed bottom-4 right-4 shadow-lg bg-blue-500 hover:bg-blue-600 dark:text-white rounded-full'
size='sm' size='sm'
> >
<MessageSquarePlus className='h-5 w-5 mr-1' /> <MessageSquarePlus className='h-5 w-5 mr-1' />

2
web/components/shared/footer.tsx

@ -4,7 +4,7 @@ import { MessageSquarePlus } from 'lucide-react'
import Link from 'next/link' import Link from 'next/link'
export default function Footer() { export default function Footer() {
return ( return (
<footer className='border-t py-6 bg-gray-50'>
<footer className='border-t py-6 bg-gray-50 dark:bg-muted'>
<div className='container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row'> <div className='container mx-auto px-4 sm:px-6 lg:px-8 max-w-7xl flex flex-col items-center justify-between gap-4 md:h-24 md:flex-row'>
<div className='flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0'> <div className='flex flex-col items-center gap-4 px-8 md:flex-row md:gap-2 md:px-0'>
<MessageSquarePlus className='h-6 w-6 text-blue-500' /> <MessageSquarePlus className='h-6 w-6 text-blue-500' />

41
web/components/shared/theme-toggle.tsx

@ -0,0 +1,41 @@
'use client'
import { useTheme } from 'next-themes'
import { Button } from '@/components/ui/button'
import { Sun, Moon, Laptop } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
export default function ThemeToggle() {
const { theme, setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='ghost' size='icon'>
<Sun className='h-5 w-5 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0' />
<Moon className='absolute h-5 w-5 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100' />
<span className='sr-only'>Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuItem onClick={() => setTheme('light')}>
<Sun className='mr-2 h-4 w-4' />
<span>Light</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
<Moon className='mr-2 h-4 w-4' />
<span>Dark</span>
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
<Laptop className='mr-2 h-4 w-4' />
<span>System</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

8
web/middleware.ts

@ -40,7 +40,13 @@ export async function middleware(request: NextRequest) {
const response = NextResponse.next() const response = NextResponse.next()
response.headers.set('x-pathname', pathname) response.headers.set('x-pathname', pathname)
return response
request.headers?.set('x-current-url', request.nextUrl?.href ?? '')
return NextResponse.next({
request: {
headers: request.headers,
},
})
} }
export const config = { export const config = {

1
web/package.json

@ -36,6 +36,7 @@
"lucide-react": "^0.453.0", "lucide-react": "^0.453.0",
"next": "14.1.0", "next": "14.1.0",
"next-auth": "^4.24.10", "next-auth": "^4.24.10",
"next-themes": "^0.4.3",
"nodemailer": "^6.9.16", "nodemailer": "^6.9.16",
"prisma": "^5.22.0", "prisma": "^5.22.0",
"react": "^18.2.0", "react": "^18.2.0",

14
web/pnpm-lock.yaml

@ -80,6 +80,9 @@ importers:
next-auth: next-auth:
specifier: ^4.24.10 specifier: ^4.24.10
version: 4.24.10(next@14.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0) version: 4.24.10(next@14.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(nodemailer@6.9.16)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-themes:
specifier: ^0.4.3
version: 0.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
nodemailer: nodemailer:
specifier: ^6.9.16 specifier: ^6.9.16
version: 6.9.16 version: 6.9.16
@ -1780,6 +1783,12 @@ packages:
nodemailer: nodemailer:
optional: true optional: true
next-themes@0.4.3:
resolution: {integrity: sha512-nG84VPkTdUHR2YeD89YchvV4I9RbiMAql3GiLEQlPvq1ioaqPaIReK+yMRdg/zgiXws620qS1rU30TiWmmG9lA==}
peerDependencies:
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
next@14.1.0: next@14.1.0:
resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==} resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==}
engines: {node: '>=18.17.0'} engines: {node: '>=18.17.0'}
@ -4212,6 +4221,11 @@ snapshots:
optionalDependencies: optionalDependencies:
nodemailer: 6.9.16 nodemailer: 6.9.16
next-themes@0.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
next@14.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0): next@14.1.0(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies: dependencies:
'@next/env': 14.1.0 '@next/env': 14.1.0

34
web/styles/main.css

@ -30,25 +30,25 @@
--radius: 0.5rem --radius: 0.5rem
} }
.dark { .dark {
--background: 224 71.4% 4.1%;
--foreground: 210 20% 98%;
--card: 224 71.4% 4.1%;
--card-foreground: 210 20% 98%;
--popover: 224 71.4% 4.1%;
--popover-foreground: 210 20% 98%;
--primary: 210 20% 98%;
--primary-foreground: 220.9 39.3% 11%;
--secondary: 215 27.9% 16.9%;
--secondary-foreground: 210 20% 98%;
--muted: 215 27.9% 16.9%;
--muted-foreground: 217.9 10.6% 64.9%;
--accent: 215 27.9% 16.9%;
--accent-foreground: 210 20% 98%;
--background: 0 0% 15%;
--foreground: 0 0% 98%;
--card: 0 0% 15%;
--card-foreground: 0 0% 98%;
--popover: 0 0% 15%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 0 0% 15%;
--secondary: 0 0% 25%;
--secondary-foreground: 0 0% 98%;
--muted: 0 0% 25%;
--muted-foreground: 0 0% 65%;
--accent: 0 0% 25%;
--accent-foreground: 0 0% 98%;
--destructive: 0 62.8% 30.6%; --destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 20% 98%; --destructive-foreground: 210 20% 98%;
--border: 215 27.9% 16.9%;
--input: 215 27.9% 16.9%;
--ring: 216 12.2% 83.9%;
--border: 0 0% 25%;
--input: 0 0% 25%;
--ring: 0 0% 83%;
--chart-1: 220 70% 50%; --chart-1: 220 70% 50%;
--chart-2: 160 60% 45%; --chart-2: 160 60% 45%;
--chart-3: 30 80% 55%; --chart-3: 30 80% 55%;

Loading…
Cancel
Save