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",
"ioredis": "^4.28.5",
"next": "^14.2.8",
"next-themes": "^0.3.0",
"nodemon": "^1.19.4",
"peer": "^0.5.3",
"peerjs": "^1.5.4",
@ -33,7 +34,7 @@
"react": "^18.2.0",
"react-device-detect": "^1.17.0",
"react-dom": "^18.2.0",
"react-qr-code": "^1.1.1",
"react-qr-code": "^2.0.15",
"streamsaver": "^2.0.6",
"tailwindcss": "^3.4.10",
"twilio": "^2.11.1",

File diff suppressed because it is too large Load Diff

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

@ -8,7 +8,7 @@ export default function CancelButton({
return (
<button
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
</button>

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

@ -16,12 +16,12 @@ export function CopyableInput({
<InputLabel>{label}</InputLabel>
<div className="flex w-full">
<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}
readOnly
/>
<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}
>
{hasCopied ? 'Copied' : 'Copy'}

@ -8,7 +8,7 @@ export default function DownloadButton({
return (
<button
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
</button>

@ -97,10 +97,10 @@ export default function DropZone({
multiple
/>
<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}
>
<span className="text-center text-stone-700">
<span className="text-center text-stone-700 dark:text-stone-200">
Drop a file to get started
</span>
</button>

@ -14,7 +14,7 @@ function FooterLink({
}): JSX.Element {
return (
<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}
target="_blank"
rel="noopener noreferrer"
@ -32,10 +32,10 @@ export function Footer(): JSX.Element {
return (
<>
<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 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!{' '}
</p>
<button
@ -46,7 +46,7 @@ export function Footer(): JSX.Element {
</button>
</div>
<p className="text-stone-600">
<p className="text-stone-600 dark:text-stone-400">
Cooked up by{' '}
<FooterLink href="http://kern.io">Alex Kern</FooterLink> &amp;{' '}
<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 {
return (
<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 mt-2">{text}</p>
<p className="text-sm text-stone-600 dark:text-stone-400 mt-2">{text}</p>
</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
autoFocus
type="password"
className={`w-full px-3 py-2 border rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 ${
isInvalid ? 'border-red-500' : 'border-stone-300'
}`}
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 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..."
value={value}
onChange={handleChange}

@ -11,7 +11,7 @@ export default function ProgressBar({
const isComplete = value === max
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">
<span className="text-black font-bold">{Math.round(percentage)}%</span>
</div>

@ -3,7 +3,10 @@ import Link from 'next/link'
export default function ReturnHome(): JSX.Element {
return (
<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;
</Link>
</div>

@ -9,7 +9,7 @@ export default function StopButton({
}): React.ReactElement {
return (
<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}
>
<svg

@ -5,7 +5,11 @@ interface SubtitleTextProps {
}
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

@ -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
}): JSX.Element {
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'
function getTypeColor(fileType: string): string {
if (fileType.startsWith('image/')) return 'bg-blue-100 text-blue-800'
if (fileType.startsWith('text/')) return 'bg-green-100 text-green-800'
if (fileType.startsWith('audio/')) return 'bg-purple-100 text-purple-800'
if (fileType.startsWith('video/')) return 'bg-red-100 text-red-800'
return 'bg-stone-100 text-stone-800'
if (fileType.startsWith('image/'))
return 'bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200'
if (fileType.startsWith('text/'))
return 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200'
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 {
return (
<span
<div
className={`px-2 py-1 text-[10px] font-semibold rounded ${getTypeColor(
type,
)} transition-all duration-300`}
>
{type}
</span>
</div>
)
}

@ -16,16 +16,18 @@ export default function UploadFileList({
const items = files.map((f: UploadedFileLike, i: number) => (
<div
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">
<p className="truncate text-sm font-medium">{f.fileName}</p>
<div className="flex items-end">
<p className="truncate text-sm font-medium text-stone-800 dark:text-stone-200">
{f.fileName}
</p>
<div className="flex items-center">
<TypeBadge type={f.type} />
{onRemove && (
<button
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>
@ -36,7 +38,7 @@ export default function UploadFileList({
))
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}
</div>
)

@ -49,9 +49,9 @@ export default function Uploader({
<CopyableInput label="Short URL" value={shortURL ?? ''} />
</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">
<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
</h2>
<StopButton onClick={onStop} />

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

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

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

Loading…
Cancel
Save