Browse Source

Merge pull request #28 from vernu/dark-mode

dark mode support
pull/29/head
Israel Abebe 1 year ago
committed by GitHub
parent
commit
4786feaf80
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  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. 3
      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. 10
      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 (
<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'>
<CardHeader className='space-y-1'>
<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 (
<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'>
<CardHeader className='space-y-1'>
<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() {
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'>
<CardHeader className='space-y-1'>
<CardTitle className='text-2xl font-bold text-center'>
@ -34,7 +34,7 @@ export default function LoginPage() {
<span className='w-full border-t' />
</div>
<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
</span>
</div>

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

@ -16,7 +16,7 @@ import { Routes } from '@/config/routes'
export default function RegisterPage() {
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'>
<CardHeader className='space-y-1'>
<CardTitle className='text-2xl font-bold text-center'>
@ -28,6 +28,16 @@ export default function RegisterPage() {
</CardHeader>
<CardContent>
<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'>
<LoginWithGoogle />
</div>

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

@ -87,7 +87,7 @@ export default function GenerateApiKey() {
>
{isGeneratingApiKey ? (
<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>
) : (
'Generate API Key'
@ -111,7 +111,7 @@ export default function GenerateApiKey() {
</DialogHeader>
<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 && (
<QRCode value={generatedApiKey?.data} size={120} />
)}

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

@ -8,6 +8,7 @@ import { ApiEndpoints } from '@/config/api'
import { useEffect } from 'react'
import { usePathname, useRouter } from 'next/navigation'
import { Routes } from '@/config/routes'
import { ThemeProvider } from 'next-themes'
export default function LayoutWrapper({ session, children }) {
const router = useRouter()
@ -35,6 +36,7 @@ export default function LayoutWrapper({ session, children }) {
return (
<>
<ThemeProvider attribute='class' defaultTheme='system' enableSystem>
<SessionProvider session={session}>
<QueryClientProvider client={queryClient}>
<GoogleOAuthProvider
@ -44,6 +46,7 @@ export default function LayoutWrapper({ session, 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}>
<AppHeader />
{children}
<main className='min-h-[80vh]'>{children}</main>
</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 { dark } from 'react-syntax-highlighter/dist/esm/styles/prism'
const codeSnippets = [
{
@ -56,10 +64,10 @@ print(response.json())`,
export default function CodeSnippetSection() {
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]'>
<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'>
<TabsList className='grid w-full grid-cols-3'>
{codeSnippets.map((snippet) => {
@ -76,6 +84,7 @@ export default function CodeSnippetSection() {
<SyntaxHighlighter
language={snippet.language}
showLineNumbers={snippet.language !== 'bash'}
style={dark}
// className='min-h-[200px]'
>
{snippet.snippet}

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

@ -4,7 +4,7 @@ import Link from 'next/link'
export default function CustomizationSection() {
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='mx-auto max-w-3xl text-center mb-12'>
<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 (
<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='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'>
<Image
alt='App preview'
@ -24,7 +24,7 @@ export default function DownloadAppSection() {
Gateway.
</p>
<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
</Button>
</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'>
<Send className='h-12 w-12 mb-4 text-blue-500' />
<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
</p>
</Card>

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

@ -6,7 +6,7 @@ import Link from 'next/link'
export default function HeroSection() {
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='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'>
@ -26,7 +26,7 @@ export default function HeroSection() {
</div>
<div className='flex flex-cdol gap-4 flex-row'>
<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
</Button>
</Link>

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

@ -10,7 +10,7 @@ export default function HowItWorksSection() {
return (
<section
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]'>
<h2 className='text-3xl font-bold text-center mb-8'>How It Works</h2>

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

@ -3,10 +3,13 @@ import { MessageSquarePlus, Moon } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { ExternalLinks } from '@/config/external-links'
import { Routes } from '@/config/routes'
import { ThemeProvider } from 'next-themes'
import ThemeToggle from '@/components/shared/theme-toggle'
export default function LandingPageHeader() {
return (
<header className='sticky top-0 z-50 w-full border-b bg-white/95 backdrop-blur supports-[backdrop-filter]:bg-white/60'>
<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
className='flex items-center space-x-2'
@ -18,6 +21,8 @@ export default function LandingPageHeader() {
</span>
</Link>
<nav className='flex items-center space-x-4'>
<ThemeToggle />
{/* <Button variant='ghost' size='icon'>
<Moon className='h-4 w-4' />
<span className='sr-only'>Toggle theme</span>
@ -33,7 +38,7 @@ export default function LandingPageHeader() {
className='text-sm font-medium hover:text-blue-500'
href={Routes.dashboard}
>
<Button className='bg-blue-500 hover:bg-blue-600 rounded-full'>
<Button className='bg-blue-500 hover:bg-blue-600 dark:text-white rounded-full'>
Go to Dashboard
</Button>
</Link>
@ -46,5 +51,6 @@ export default function LandingPageHeader() {
</nav>
</div>
</header>
</ThemeProvider>
)
}

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

@ -52,7 +52,7 @@ export default function SupportProjectSection() {
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'>
<h2 className='text-3xl font-bold mb-4'>Support The Project</h2>
<p className='text-gray-500 mb-8'>
@ -64,7 +64,7 @@ export default function SupportProjectSection() {
</p>
<div className='flex flex-col sm:flex-row justify-center gap-4'>
<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
</Button>
</Link>
@ -84,7 +84,7 @@ export default function SupportProjectSection() {
{cryptoWallets.map((wallet, index) => (
<div
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>
<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 { headers } from 'next/dist/client/components/headers'
import { authOptions } from '@/lib/auth'
import { PrismaClient } from '@prisma/client'
import prismaClient from '@/lib/prismaClient'
import { userAgent } from 'next/server'
export const metadata: Metadata = {
title: 'textbee.dev - Free and Open-Source SMS Gateway',
@ -59,8 +62,46 @@ export const metadata: Metadata = {
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) {
const session: Session | null = await getServerSession(authOptions as any)
const headerList = headers()
trackPageView({ headerList, session })
.catch(console.error)
.then((res) => {
// console.log(res)
})
return (
<html lang='en'>
<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 { signOut, useSession } from 'next-auth/react'
import { Routes } from '@/config/routes'
import ThemeToggle from './theme-toggle'
export default function AppHeader() {
const session = useSession()
@ -44,7 +45,9 @@ export default function AppHeader() {
{session.data?.user?.name?.charAt(0)}
</AvatarFallback>
</Avatar>
<div className='hidden md:block'>{session.data?.user?.name}</div>
<div className='hidden md:block'>
{session.data?.user?.name?.split(' ')[0]}
</div>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className='w-56' align='end' forceMount>
@ -122,7 +125,7 @@ export default function AppHeader() {
<Button
asChild
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>
</Button>
@ -148,7 +151,8 @@ export default function AppHeader() {
</Link>
</div>
<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 ? (
<AuthenticatedMenu />
) : (

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

@ -88,7 +88,7 @@ export default function SupportButton() {
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogTrigger asChild>
<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'
>
<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'
export default function Footer() {
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='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' />

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()
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 = {

1
web/package.json

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

14
web/pnpm-lock.yaml

@ -80,6 +80,9 @@ importers:
next-auth:
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)
next-themes:
specifier: ^0.4.3
version: 0.4.3(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
nodemailer:
specifier: ^6.9.16
version: 6.9.16
@ -1780,6 +1783,12 @@ packages:
nodemailer:
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:
resolution: {integrity: sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==}
engines: {node: '>=18.17.0'}
@ -4212,6 +4221,11 @@ snapshots:
optionalDependencies:
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):
dependencies:
'@next/env': 14.1.0

34
web/styles/main.css

@ -30,25 +30,25 @@
--radius: 0.5rem
}
.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-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-2: 160 60% 45%;
--chart-3: 30 80% 55%;

Loading…
Cancel
Save