Add dark mode

pull/134/head
Alex Kern 1 year ago
parent 3fb4af543f
commit eeb1869cfc
No known key found for this signature in database
GPG Key ID: EF051FACCACBEE25

@ -26,6 +26,7 @@
"express": "^4.19.2", "express": "^4.19.2",
"ioredis": "^4.28.5", "ioredis": "^4.28.5",
"next": "^14.2.8", "next": "^14.2.8",
"next-themes": "^0.3.0",
"nodemon": "^1.19.4", "nodemon": "^1.19.4",
"peer": "^0.5.3", "peer": "^0.5.3",
"peerjs": "^1.5.4", "peerjs": "^1.5.4",
@ -33,7 +34,7 @@
"react": "^18.2.0", "react": "^18.2.0",
"react-device-detect": "^1.17.0", "react-device-detect": "^1.17.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-qr-code": "^1.1.1", "react-qr-code": "^2.0.15",
"streamsaver": "^2.0.6", "streamsaver": "^2.0.6",
"tailwindcss": "^3.4.10", "tailwindcss": "^3.4.10",
"twilio": "^2.11.1", "twilio": "^2.11.1",

File diff suppressed because it is too large Load Diff

@ -1,6 +1,8 @@
import React from 'react' import React from 'react'
import Footer from '../components/Footer' import Footer from '../components/Footer'
import '../styles.css' import '../styles.css'
import { ThemeProvider } from '../components/ThemeProvider'
import { ModeToggle } from '../components/ModeToggle'
export const metadata = { export const metadata = {
title: 'FilePizza • Your files, delivered.', title: 'FilePizza • Your files, delivered.',
@ -21,13 +23,16 @@ export default function RootLayout({
children: React.ReactNode children: React.ReactNode
}): React.ReactElement { }): React.ReactElement {
return ( return (
<html lang="en"> <html lang="en" suppressHydrationWarning>
<head> <head>
<meta name="monetization" content="$twitter.xrptipbot.com/kernio" /> <meta name="monetization" content="$twitter.xrptipbot.com/kernio" />
</head> </head>
<body> <body>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<main>{children}</main> <main>{children}</main>
<Footer /> <Footer />
<ModeToggle />
</ThemeProvider>
</body> </body>
</html> </html>
) )

@ -8,7 +8,7 @@ export default function CancelButton({
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className="px-4 py-2 text-sm font-medium text-stone-700 bg-white border border-stone-300 rounded-md hover:bg-stone-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500" className="px-4 py-2 text-sm font-medium text-stone-700 dark:text-stone-200 bg-white dark:bg-stone-800 border border-stone-300 dark:border-stone-600 rounded-md hover:bg-stone-50 dark:hover:bg-stone-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-indigo-400"
> >
Cancel Cancel
</button> </button>

@ -10,15 +10,15 @@ export function ConnectionListItem({
const getStatusColor = (status: UploaderConnectionStatus) => { const getStatusColor = (status: UploaderConnectionStatus) => {
switch (status) { switch (status) {
case UploaderConnectionStatus.Uploading: case UploaderConnectionStatus.Uploading:
return 'bg-blue-500' return 'bg-blue-500 dark:bg-blue-600'
case UploaderConnectionStatus.Paused: case UploaderConnectionStatus.Paused:
return 'bg-yellow-500' return 'bg-yellow-500 dark:bg-yellow-600'
case UploaderConnectionStatus.Done: case UploaderConnectionStatus.Done:
return 'bg-green-500' return 'bg-green-500 dark:bg-green-600'
case UploaderConnectionStatus.Closed: case UploaderConnectionStatus.Closed:
return 'bg-red-500' return 'bg-red-500 dark:bg-red-600'
default: default:
return 'bg-gray-500' return 'bg-stone-500 dark:bg-stone-600'
} }
} }

@ -16,12 +16,12 @@ export function CopyableInput({
<InputLabel>{label}</InputLabel> <InputLabel>{label}</InputLabel>
<div className="flex w-full"> <div className="flex w-full">
<input <input
className="flex-grow px-3 py-2 text-xs border border-r-0 rounded-l" className="flex-grow px-3 py-2 text-xs border border-r-0 rounded-l text-stone-900 dark:text-stone-100 bg-white dark:bg-stone-800 border-stone-300 dark:border-stone-600"
value={value} value={value}
readOnly readOnly
/> />
<button <button
className="px-4 py-2 text-sm text-stone-700 bg-stone-100 hover:bg-stone-200 rounded-r border-t border-r border-b" className="px-4 py-2 text-sm text-stone-700 dark:text-stone-200 bg-stone-100 dark:bg-stone-700 hover:bg-stone-200 dark:hover:bg-stone-600 rounded-r border-t border-r border-b border-stone-300 dark:border-stone-600"
onClick={onCopy} onClick={onCopy}
> >
{hasCopied ? 'Copied' : 'Copy'} {hasCopied ? 'Copied' : 'Copy'}

@ -8,7 +8,7 @@ export default function DownloadButton({
return ( return (
<button <button
onClick={onClick} onClick={onClick}
className="px-4 py-2 bg-gradient-to-b from-green-500 to-green-600 text-white rounded-md hover:from-green-500 hover:to-green-700 transition-all duration-200 border border-green-600 shadow-sm hover:shadow-md text-shadow" className="h-12 px-4 bg-gradient-to-b from-green-500 to-green-600 text-white rounded-md hover:from-green-500 hover:to-green-700 transition-all duration-200 border border-green-600 shadow-sm hover:shadow-md text-shadow"
> >
Download Download
</button> </button>

@ -97,10 +97,10 @@ export default function DropZone({
multiple multiple
/> />
<button <button
className="inline-block cursor-pointer relative py-3 px-6 text-base font-bold text-stone-700 bg-white border-2 border-stone-700 rounded-lg transition-all duration-300 ease-in-out outline-none hover:shadow-md active:shadow-inner focus:shadow-outline" className="block cursor-pointer relative py-3 px-6 text-base font-bold text-stone-700 dark:text-stone-200 bg-white dark:bg-stone-800 border-2 border-stone-700 dark:border-stone-700 rounded-lg transition-all duration-300 ease-in-out outline-none hover:shadow-md active:shadow-inner focus:shadow-outline"
onClick={handleClick} onClick={handleClick}
> >
<span className="text-center text-stone-700"> <span className="text-center text-stone-700 dark:text-stone-200">
Drop a file to get started Drop a file to get started
</span> </span>
</button> </button>

@ -14,7 +14,7 @@ function FooterLink({
}): JSX.Element { }): JSX.Element {
return ( return (
<a <a
className="text-stone-600 underline hover:text-stone-800" className="text-stone-600 dark:text-stone-400 underline hover:text-stone-800 dark:hover:text-stone-200"
href={href} href={href}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
@ -32,10 +32,10 @@ export function Footer(): JSX.Element {
return ( return (
<> <>
<div className="h-[100px]" /> {/* Spacer to account for footer height */} <div className="h-[100px]" /> {/* Spacer to account for footer height */}
<footer className="fixed bottom-0 left-0 right-0 text-center py-2.5 pb-4 text-xs border-t border-stone-200 shadow-[0_-1px_2px_rgba(0,0,0,0.04)] bg-white"> <footer className="fixed bottom-0 left-0 right-0 text-center py-2.5 pb-4 text-xs border-t border-stone-200 dark:border-stone-700 shadow-[0_-1px_2px_rgba(0,0,0,0.04)] dark:shadow-[0_-1px_2px_rgba(255,255,255,0.04)] bg-white dark:bg-stone-900">
<div className="flex flex-col items-center space-y-1 px-4 sm:px-6 md:px-8"> <div className="flex flex-col items-center space-y-1 px-4 sm:px-6 md:px-8">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<p className="text-stone-600"> <p className="text-stone-600 dark:text-stone-400">
<strong>Like FilePizza?</strong> Support its development!{' '} <strong>Like FilePizza?</strong> Support its development!{' '}
</p> </p>
<button <button
@ -46,7 +46,7 @@ export function Footer(): JSX.Element {
</button> </button>
</div> </div>
<p className="text-stone-600"> <p className="text-stone-600 dark:text-stone-400">
Cooked up by{' '} Cooked up by{' '}
<FooterLink href="http://kern.io">Alex Kern</FooterLink> &amp;{' '} <FooterLink href="http://kern.io">Alex Kern</FooterLink> &amp;{' '}
<FooterLink href="http://neeraj.io">Neeraj Baid</FooterLink> while <FooterLink href="http://neeraj.io">Neeraj Baid</FooterLink> while

@ -3,8 +3,7 @@ import React from 'react'
export default function Loading({ text }: { text: string }): JSX.Element { export default function Loading({ text }: { text: string }): JSX.Element {
return ( return (
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className="w-8 h-8 border-4 border-blue-500 border-t-transparent rounded-full animate-spin"></div> <p className="text-sm text-stone-600 dark:text-stone-400 mt-2">{text}</p>
<p className="text-sm text-stone-600 mt-2">{text}</p>
</div> </div>
) )
} }

@ -0,0 +1,56 @@
'use client'
import { useTheme } from 'next-themes'
function LightModeIcon(): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4 block dark:hidden"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M5.25 12H3m4.227-4.773L5.636 5.636M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
/>
</svg>
)
}
function DarkModeIcon(): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth={1.5}
stroke="currentColor"
className="w-4 h-4 hidden dark:block"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
/>
</svg>
)
}
export function ModeToggle(): JSX.Element {
const { setTheme, resolvedTheme } = useTheme()
return (
<button
onClick={() => setTheme(resolvedTheme === 'dark' ? 'light' : 'dark')}
className="fixed top-4 right-4 border rounded-md w-6 h-6 flex items-center justify-center"
>
<span className="sr-only">Toggle mode</span>
<LightModeIcon />
<DarkModeIcon />
</button>
)
}

@ -27,9 +27,11 @@ export default function PasswordField({
<input <input
autoFocus autoFocus
type="password" type="password"
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${ className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 dark:focus:ring-blue-400 ${
isInvalid ? 'border-red-500' : 'border-stone-300' isInvalid
}`} ? 'border-red-500 dark:border-red-400'
: 'border-stone-300 dark:border-stone-600'
} bg-white dark:bg-stone-800 text-stone-900 dark:text-stone-100`}
placeholder="Enter a secret password for this slice of FilePizza..." placeholder="Enter a secret password for this slice of FilePizza..."
value={value} value={value}
onChange={handleChange} onChange={handleChange}

@ -11,7 +11,7 @@ export default function ProgressBar({
const isComplete = value === max const isComplete = value === max
return ( return (
<div className="w-full h-12 bg-stone-200 rounded-md overflow-hidden relative shadow-sm"> <div className="w-full h-12 bg-stone-200 dark:bg-stone-700 rounded-md overflow-hidden relative shadow-sm">
<div className="absolute inset-0 flex items-center justify-center"> <div className="absolute inset-0 flex items-center justify-center">
<span className="text-black font-bold">{Math.round(percentage)}%</span> <span className="text-black font-bold">{Math.round(percentage)}%</span>
</div> </div>

@ -3,7 +3,10 @@ import Link from 'next/link'
export default function ReturnHome(): JSX.Element { export default function ReturnHome(): JSX.Element {
return ( return (
<div className="flex justify-center"> <div className="flex justify-center">
<Link href="/" className="text-stone-500 hover:underline"> <Link
href="/"
className="text-stone-500 dark:text-stone-200 hover:underline"
>
Serve up a fresh slice &raquo; Serve up a fresh slice &raquo;
</Link> </Link>
</div> </div>

@ -9,7 +9,7 @@ export default function StopButton({
}): React.ReactElement { }): React.ReactElement {
return ( return (
<button <button
className="px-2 py-1 text-xs text-orange-500 bg-transparent hover:bg-orange-100 rounded transition-colors duration-200 flex items-center" className="px-2 py-1 text-xs text-orange-500 dark:text-orange-400 bg-transparent hover:bg-orange-100 dark:hover:bg-orange-900 rounded transition-colors duration-200 flex items-center"
onClick={onClick} onClick={onClick}
> >
<svg <svg

@ -5,7 +5,11 @@ interface SubtitleTextProps {
} }
const SubtitleText: React.FC<SubtitleTextProps> = ({ children }) => { const SubtitleText: React.FC<SubtitleTextProps> = ({ children }) => {
return <p className="text-sm text-center text-stone-600">{children}</p> return (
<p className="text-sm text-center text-stone-600 dark:text-stone-400">
{children}
</p>
)
} }
export default SubtitleText export default SubtitleText

@ -0,0 +1,9 @@
"use client";
import * as React from "react";
import { ThemeProvider as NextThemesProvider } from "next-themes";
import type { ThemeProviderProps } from "next-themes/dist/types";
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
}

@ -6,6 +6,8 @@ export default function TitleText({
children: React.ReactNode children: React.ReactNode
}): JSX.Element { }): JSX.Element {
return ( return (
<p className="text-lg text-center text-stone-800 max-w-md">{children}</p> <p className="text-lg text-center text-stone-800 dark:text-stone-200 max-w-md">
{children}
</p>
) )
} }

@ -1,21 +1,25 @@
import React from 'react' import React from 'react'
function getTypeColor(fileType: string): string { function getTypeColor(fileType: string): string {
if (fileType.startsWith('image/')) return 'bg-blue-100 text-blue-800' if (fileType.startsWith('image/'))
if (fileType.startsWith('text/')) return 'bg-green-100 text-green-800' return 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
if (fileType.startsWith('audio/')) return 'bg-purple-100 text-purple-800' if (fileType.startsWith('text/'))
if (fileType.startsWith('video/')) return 'bg-red-100 text-red-800' return 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200'
return 'bg-stone-100 text-stone-800' if (fileType.startsWith('audio/'))
return 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200'
if (fileType.startsWith('video/'))
return 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200'
return 'bg-stone-100 dark:bg-stone-900 text-stone-800 dark:text-stone-200'
} }
export default function TypeBadge({ type }: { type: string }): JSX.Element { export default function TypeBadge({ type }: { type: string }): JSX.Element {
return ( return (
<span <div
className={`px-2 py-1 text-[10px] font-semibold rounded ${getTypeColor( className={`px-2 py-1 text-[10px] font-semibold rounded ${getTypeColor(
type, type,
)} transition-all duration-300`} )} transition-all duration-300`}
> >
{type} {type}
</span> </div>
) )
} }

@ -16,16 +16,18 @@ export default function UploadFileList({
const items = files.map((f: UploadedFileLike, i: number) => ( const items = files.map((f: UploadedFileLike, i: number) => (
<div <div
key={f.fileName} key={f.fileName}
className={`w-full border-b border-stone-300 last:border-0`} className={`w-full border-b border-stone-300 dark:border-stone-700 last:border-0`}
> >
<div className="flex justify-between items-center py-2 pl-3 pr-2"> <div className="flex justify-between items-center py-2 pl-3 pr-2">
<p className="truncate text-sm font-medium">{f.fileName}</p> <p className="truncate text-sm font-medium text-stone-800 dark:text-stone-200">
<div className="flex items-end"> {f.fileName}
</p>
<div className="flex items-center">
<TypeBadge type={f.type} /> <TypeBadge type={f.type} />
{onRemove && ( {onRemove && (
<button <button
onClick={() => onRemove?.(i)} onClick={() => onRemove?.(i)}
className="text-stone-500 hover:text-stone-700 focus:outline-none pl-3 pr-1" className="text-stone-500 hover:text-stone-700 dark:text-stone-400 dark:hover:text-stone-200 focus:outline-none pl-3 pr-1"
> >
</button> </button>
@ -36,7 +38,7 @@ export default function UploadFileList({
)) ))
return ( return (
<div className="w-full border border-stone-300 rounded-md shadow-sm"> <div className="w-full border border-stone-300 dark:border-stone-700 rounded-md shadow-sm dark:shadow-sm-dark bg-white dark:bg-stone-800">
{items} {items}
</div> </div>
) )

@ -49,9 +49,9 @@ export default function Uploader({
<CopyableInput label="Short URL" value={shortURL ?? ''} /> <CopyableInput label="Short URL" value={shortURL ?? ''} />
</div> </div>
</div> </div>
<div className="mt-6 pt-4 border-t border-gray-200 w-full"> <div className="mt-6 pt-4 border-t border-stone-200 dark:border-stone-700 w-full">
<div className="flex justify-between items-center mb-2"> <div className="flex justify-between items-center mb-2">
<h2 className="text-lg font-semibold text-stone-400"> <h2 className="text-lg font-semibold text-stone-400 dark:text-stone-200">
{activeDownloaders} Downloading, {connections.length} Total {activeDownloaders} Downloading, {connections.length} Total
</h2> </h2>
<StopButton onClick={onStop} /> <StopButton onClick={onStop} />

@ -67,7 +67,7 @@ export function WebRTCProvider({
}, [servers]) }, [servers])
if (!loaded || !peer.current) { if (!loaded || !peer.current) {
return <Loading text="Initializing WebRTC" /> return <Loading text="Initializing WebRTC connection..." />
} }
return ( return (

@ -5,7 +5,7 @@ export default function Wordmark(): JSX.Element {
return ( return (
<Image <Image
src="/images/wordmark.png" src="/images/wordmark.png"
className="max-h-12" className="max-h-12 dark:brightness-0 dark:invert"
alt="FilePizza Wordmark" alt="FilePizza Wordmark"
width={200} width={200}
height={45} height={45}

@ -5,4 +5,5 @@ export default {
extend: {}, extend: {},
}, },
plugins: [], plugins: [],
darkMode: 'class',
} }

Loading…
Cancel
Save