improve styling

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

3
next-env.d.ts vendored

@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference types="next/navigation-types/compat/navigation" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.
// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information.

@ -0,0 +1,5 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
}
module.exports = nextConfig

@ -28,14 +28,14 @@
"immer": "^8.0.4",
"io-ts": "^2.2.21",
"ioredis": "^4.28.5",
"next": "^14.2.7",
"next": "^14.2.8",
"nodemon": "^1.19.4",
"peer": "^0.5.3",
"peerjs": "^1.5.4",
"postcss": "^8.4.44",
"react": "^18.3.1",
"react": "^18.2.0",
"react-device-detect": "^1.17.0",
"react-dom": "^18.3.1",
"react-dom": "^18.2.0",
"react-qr": "0.0.2",
"react-qr-code": "^1.1.1",
"streamsaver": "^2.0.6",
@ -43,8 +43,7 @@
"twilio": "^2.11.1",
"use-http": "^1.0.28",
"web-streams-polyfill": "^3.3.3",
"webrtcsupport": "^2.2.0",
"xkcd-password": "^1.2.0"
"webrtcsupport": "^2.2.0"
},
"devDependencies": {
"@types/debug": "^4.1.12",

@ -30,8 +30,8 @@ dependencies:
specifier: ^4.28.5
version: 4.28.5
next:
specifier: ^14.2.7
version: 14.2.7(@babel/core@7.25.2)(react-dom@18.3.1)(react@18.3.1)
specifier: ^14.2.8
version: 14.2.8(@babel/core@7.25.2)(react-dom@18.3.1)(react@18.3.1)
nodemon:
specifier: ^1.19.4
version: 1.19.4
@ -45,13 +45,13 @@ dependencies:
specifier: ^8.4.44
version: 8.4.44
react:
specifier: ^18.3.1
specifier: ^18.2.0
version: 18.3.1
react-device-detect:
specifier: ^1.17.0
version: 1.17.0(react-dom@18.3.1)(react@18.3.1)
react-dom:
specifier: ^18.3.1
specifier: ^18.2.0
version: 18.3.1(react@18.3.1)
react-qr:
specifier: 0.0.2
@ -77,9 +77,6 @@ dependencies:
webrtcsupport:
specifier: ^2.2.0
version: 2.2.0
xkcd-password:
specifier: ^1.2.0
version: 1.2.0
devDependencies:
'@types/debug':
@ -1773,12 +1770,12 @@ packages:
engines: {node: '>= 10'}
dev: false
/@next/env@14.2.7:
resolution: {integrity: sha512-OTx9y6I3xE/eih+qtthppwLytmpJVPM5PPoJxChFsbjIEFXIayG0h/xLzefHGJviAa3Q5+Fd+9uYojKkHDKxoQ==}
/@next/env@14.2.8:
resolution: {integrity: sha512-L44a+ynqkolyNBnYfF8VoCiSrjSZWgEHYKkKLGcs/a80qh7AkfVUD/MduVPgdsWZ31tgROR+yJRA0PZjSVBXWQ==}
dev: false
/@next/swc-darwin-arm64@14.2.7:
resolution: {integrity: sha512-UhZGcOyI9LE/tZL3h9rs/2wMZaaJKwnpAyegUVDGZqwsla6hMfeSj9ssBWQS9yA4UXun3pPhrFLVnw5KXZs3vw==}
/@next/swc-darwin-arm64@14.2.8:
resolution: {integrity: sha512-1VrQlG8OzdyvvGZhGJFnaNE2P10Jjy/2FopnqbY0nSa/gr8If3iINxvOEW3cmVeoAYkmW0RsBazQecA2dBFOSw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
@ -1786,8 +1783,8 @@ packages:
dev: false
optional: true
/@next/swc-darwin-x64@14.2.7:
resolution: {integrity: sha512-ys2cUgZYRc+CbyDeLAaAdZgS7N1Kpyy+wo0b/gAj+SeOeaj0Lw/q+G1hp+DuDiDAVyxLBCJXEY/AkhDmtihUTA==}
/@next/swc-darwin-x64@14.2.8:
resolution: {integrity: sha512-87t3I86rNRSOJB1gXIUzaQWWSWrkWPDyZGsR0Z7JAPtLeX3uUOW2fHxl7dNWD2BZvbvftctTQjgtfpp7nMtmWg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
@ -1795,8 +1792,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-gnu@14.2.7:
resolution: {integrity: sha512-2xoWtE13sUJ3qrC1lwE/HjbDPm+kBQYFkkiVECJWctRASAHQ+NwjMzgrfqqMYHfMxFb5Wws3w9PqzZJqKFdWcQ==}
/@next/swc-linux-arm64-gnu@14.2.8:
resolution: {integrity: sha512-ta2sfVzbOpTbgBrF9HM5m+U58dv6QPuwU4n5EX4LLyCJGKc433Z0D9h9gay/HSOjLEXJ2fJYrMP5JYYbHdxhtw==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@ -1804,8 +1801,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-arm64-musl@14.2.7:
resolution: {integrity: sha512-+zJ1gJdl35BSAGpkCbfyiY6iRTaPrt3KTl4SF/B1NyELkqqnrNX6cp4IjjjxKpd64/7enI0kf6b9O1Uf3cL0pw==}
/@next/swc-linux-arm64-musl@14.2.8:
resolution: {integrity: sha512-+IoLTPK6Z5uIgDhgeWnQF5/o5GBN7+zyUNrs4Bes1W3g9++YELb8y0unFybS8s87ntAKMDl6jeQ+mD7oNwp/Ng==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
@ -1813,8 +1810,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-gnu@14.2.7:
resolution: {integrity: sha512-m6EBqrskeMUzykBrv0fDX/28lWIBGhMzOYaStp0ihkjzIYJiKUOzVYD1gULHc8XDf5EMSqoH/0/TRAgXqpQwmw==}
/@next/swc-linux-x64-gnu@14.2.8:
resolution: {integrity: sha512-pO+hVXC+mvzUOQJJRG4RX4wJsRJ5BkURSf6dD6EjUXAX4Ml9es1WsEfkaZ4lcpmFzFvY47IkDaffks/GdCn9ag==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@ -1822,8 +1819,8 @@ packages:
dev: false
optional: true
/@next/swc-linux-x64-musl@14.2.7:
resolution: {integrity: sha512-gUu0viOMvMlzFRz1r1eQ7Ql4OE+hPOmA7smfZAhn8vC4+0swMZaZxa9CSIozTYavi+bJNDZ3tgiSdMjmMzRJlQ==}
/@next/swc-linux-x64-musl@14.2.8:
resolution: {integrity: sha512-bCat9izctychCtf3uL1nqHq31N5e1VxvdyNcBQflkudPMLbxVnlrw45Vi87K+lt1CwrtVayHqzo4ie0Szcpwzg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
@ -1831,8 +1828,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-arm64-msvc@14.2.7:
resolution: {integrity: sha512-PGbONHIVIuzWlYmLvuFKcj+8jXnLbx4WrlESYlVnEzDsa3+Q2hI1YHoXaSmbq0k4ZwZ7J6sWNV4UZfx1OeOlbQ==}
/@next/swc-win32-arm64-msvc@14.2.8:
resolution: {integrity: sha512-gbxfUaSPV7EyUobpavida2Hwi62GhSJaSg7iBjmBWoxkxlmETOD7U4tWt763cGIsyE6jM7IoNavq0BXqwdW2QA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
@ -1840,8 +1837,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-ia32-msvc@14.2.7:
resolution: {integrity: sha512-BiSY5umlx9ed5RQDoHcdbuKTUkuFORDqzYKPHlLeS+STUWQKWziVOn3Ic41LuTBvqE0TRJPKpio9GSIblNR+0w==}
/@next/swc-win32-ia32-msvc@14.2.8:
resolution: {integrity: sha512-PUXzEzjTTlUh3b5VAn1nlpwvujTnuCMMwbiCnaTazoVlN1nA3kWjlmp42IfURA2N/nyrlVEw7pURa/o4Qxj1cw==}
engines: {node: '>= 10'}
cpu: [ia32]
os: [win32]
@ -1849,8 +1846,8 @@ packages:
dev: false
optional: true
/@next/swc-win32-x64-msvc@14.2.7:
resolution: {integrity: sha512-pxsI23gKWRt/SPHFkDEsP+w+Nd7gK37Hpv0ngc5HpWy2e7cKx9zR/+Q2ptAUqICNTecAaGWvmhway7pj/JLEWA==}
/@next/swc-win32-x64-msvc@14.2.8:
resolution: {integrity: sha512-EnPKv0ttq02E9/1KZ/8Dn7kuutv6hy1CKc0HlNcvzOQcm4/SQtvfws5gY0zrG9tuupd3HfC2L/zcTrnBhpjTuQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@ -2686,11 +2683,6 @@ packages:
strip-ansi: 5.2.0
dev: false
/ansi-regex@0.2.1:
resolution: {integrity: sha512-sGwIGMjhYdW26/IhwK2gkWWI8DRCVO6uj3hYgHT+zD+QL1pa37tM3ujhyfcJIYSbsxp7Gxhy7zrRW/1AHm4BmA==}
engines: {node: '>=0.10.0'}
dev: false
/ansi-regex@2.1.1:
resolution: {integrity: sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==}
engines: {node: '>=0.10.0'}
@ -2715,11 +2707,6 @@ packages:
engines: {node: '>=12'}
dev: false
/ansi-styles@1.1.0:
resolution: {integrity: sha512-f2PKUkN5QngiSemowa6Mrk9MPCdtFiOSmibjZ+j1qhLGHHYsqZwmBMRF3IRMVXo8sybDqx2fJl2d/8OphBoWkA==}
engines: {node: '>=0.10.0'}
dev: false
/ansi-styles@2.2.1:
resolution: {integrity: sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==}
engines: {node: '>=0.10.0'}
@ -2957,10 +2944,6 @@ packages:
resolution: {integrity: sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==}
dev: false
/async@0.9.2:
resolution: {integrity: sha512-l6ToIJIotphWahxxHyzK9bnLR6kM4jJIIgLShZeqLY7iboHoGkdgFl7W2/Ivi4SkMJYGKqW8vSuk0uKUj6qsSw==}
dev: false
/async@2.6.4:
resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==}
dependencies:
@ -3321,17 +3304,6 @@ packages:
resolution: {integrity: sha512-ODLXH644w9C2fMPAm7bMDQ3GRvipZWZfKc+8As6hIadRIelE0n0xZuN38NS6kiK3KPEVrpymmQD8bvncAHWQkQ==}
dev: false
/chalk@0.5.1:
resolution: {integrity: sha512-bIKA54hP8iZhyDT81TOsJiQvR1gW+ZYSXFaZUAvoD4wCHdbHY2actmpTE4x344ZlFqHbvoxKOaESULTZN2gstg==}
engines: {node: '>=0.10.0'}
dependencies:
ansi-styles: 1.1.0
escape-string-regexp: 1.0.5
has-ansi: 0.1.0
strip-ansi: 0.3.0
supports-color: 0.2.0
dev: false
/chalk@1.1.3:
resolution: {integrity: sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==}
engines: {node: '>=0.10.0'}
@ -5105,14 +5077,6 @@ packages:
pinkie-promise: 2.0.1
dev: false
/has-ansi@0.1.0:
resolution: {integrity: sha512-1YsTg1fk2/6JToQhtZkArMkurq8UoWU1Qe0aR3VUHjgij4nOylSWLWAtBXoZ4/dXOmugfLGm1c+QhuD0JyedFA==}
engines: {node: '>=0.10.0'}
hasBin: true
dependencies:
ansi-regex: 0.2.1
dev: false
/has-ansi@2.0.0:
resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==}
engines: {node: '>=0.10.0'}
@ -6751,8 +6715,8 @@ packages:
resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==}
dev: false
/next@14.2.7(@babel/core@7.25.2)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-4Qy2aK0LwH4eQiSvQWyKuC7JXE13bIopEQesWE0c/P3uuNRnZCQanI0vsrMLmUQJLAto+A+/8+sve2hd+BQuOQ==}
/next@14.2.8(@babel/core@7.25.2)(react-dom@18.3.1)(react@18.3.1):
resolution: {integrity: sha512-EyEyJZ89r8C5FPlS/401AiF3O8jeMtHIE+bLom9MwcdWJJFBgRl+MR/2VgO0v5bI6tQORNY0a0DR5sjpFNrjbg==}
engines: {node: '>=18.17.0'}
hasBin: true
peerDependencies:
@ -6769,7 +6733,7 @@ packages:
sass:
optional: true
dependencies:
'@next/env': 14.2.7
'@next/env': 14.2.8
'@swc/helpers': 0.5.5
busboy: 1.6.0
caniuse-lite: 1.0.30001655
@ -6779,15 +6743,15 @@ packages:
react-dom: 18.3.1(react@18.3.1)
styled-jsx: 5.1.1(@babel/core@7.25.2)(react@18.3.1)
optionalDependencies:
'@next/swc-darwin-arm64': 14.2.7
'@next/swc-darwin-x64': 14.2.7
'@next/swc-linux-arm64-gnu': 14.2.7
'@next/swc-linux-arm64-musl': 14.2.7
'@next/swc-linux-x64-gnu': 14.2.7
'@next/swc-linux-x64-musl': 14.2.7
'@next/swc-win32-arm64-msvc': 14.2.7
'@next/swc-win32-ia32-msvc': 14.2.7
'@next/swc-win32-x64-msvc': 14.2.7
'@next/swc-darwin-arm64': 14.2.8
'@next/swc-darwin-x64': 14.2.8
'@next/swc-linux-arm64-gnu': 14.2.8
'@next/swc-linux-arm64-musl': 14.2.8
'@next/swc-linux-x64-gnu': 14.2.8
'@next/swc-linux-x64-musl': 14.2.8
'@next/swc-win32-arm64-msvc': 14.2.8
'@next/swc-win32-ia32-msvc': 14.2.8
'@next/swc-win32-x64-msvc': 14.2.8
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
@ -7564,17 +7528,6 @@ packages:
inherits: 2.0.4
dev: false
/random-lib@1.1.3:
resolution: {integrity: sha512-W/Mkgwrbm4vFw+Fnye1+Yq9vJ0Y85RjbDYDJFfJeIyjOyVVE5Ax310aYXgcCyyGvZgZYkn0xBB5TBQK9JkfjFQ==}
engines: {node: '>=0.10.22'}
dependencies:
async: 0.9.2
debug: 2.6.9(supports-color@5.5.0)
when: 3.7.8
transitivePeerDependencies:
- supports-color
dev: false
/range-parser@1.2.1:
resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==}
engines: {node: '>= 0.6'}
@ -8576,14 +8529,6 @@ packages:
resolution: {integrity: sha512-87GEBAkegbBcweToUrdzf3eLhWNg06FJTebl4BVJz/JgWy8CvEr9dRtX5qWphiynMSQlxxi+QqN0z5T32SLlhA==}
dev: false
/strip-ansi@0.3.0:
resolution: {integrity: sha512-DerhZL7j6i6/nEnVG0qViKXI0OKouvvpsAiaj7c+LfqZZZxdwZtv8+UiA/w4VUJpT8UzX0pR1dcHOii1GbmruQ==}
engines: {node: '>=0.10.0'}
hasBin: true
dependencies:
ansi-regex: 0.2.1
dev: false
/strip-ansi@3.0.1:
resolution: {integrity: sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==}
engines: {node: '>=0.10.0'}
@ -8689,12 +8634,6 @@ packages:
resolution: {integrity: sha512-Mu7R0g4ig9TUuGSxJavny5Rv0egCEtpZRNMrZaYS1vxkiIxGiGUwoezU3LazIQ+KE04hTrTfNPgxU5gzi7F5Pw==}
dev: false
/supports-color@0.2.0:
resolution: {integrity: sha512-tdCZ28MnM7k7cJDJc7Eq80A9CsRFAAOZUy41npOZCs++qSjfIy7o5Rh46CBk+Dk5FbKJ33X3Tqg4YrV07N5RaA==}
engines: {node: '>=0.10.0'}
hasBin: true
dev: false
/supports-color@2.0.0:
resolution: {integrity: sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==}
engines: {node: '>=0.8.0'}
@ -9279,10 +9218,6 @@ packages:
webidl-conversions: 3.0.1
dev: false
/when@3.7.8:
resolution: {integrity: sha512-5cZ7mecD3eYcMiCH4wtRPA5iFJZ50BJYDfckI5RRpQiktMiYTcn0ccLTZOvcbBume+1304fQztxeNzNS9Gvrnw==}
dev: false
/which-boxed-primitive@1.0.2:
resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==}
dependencies:
@ -9435,21 +9370,6 @@ packages:
engines: {node: '>=4'}
dev: false
/xkcd-password@1.2.0:
resolution: {integrity: sha512-JWQoDCiysReS+vUnLJJ16bJakRPQY/eV1DA0+21xoZplDadlGFGQxCPP3FuWEuZ9Dq3v8cvcHKajXVdf1Cj1cQ==}
engines: {node: '>=0.10.22'}
hasBin: true
dependencies:
async: 0.9.2
chalk: 0.5.1
debug: 2.6.9(supports-color@5.5.0)
minimist: 1.2.8
random-lib: 1.1.3
when: 3.7.8
transitivePeerDependencies:
- supports-color
dev: false
/xtend@4.0.2:
resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==}
engines: {node: '>=0.4'}

@ -0,0 +1,42 @@
import { channelRepo } from '../../../channel'
import Spinner from '../../../components/Spinner'
import Wordmark from '../../../components/Wordmark'
import Downloader from '../../../components/Downloader'
import WebRTCProvider from '../../../components/WebRTCProvider'
const normalizeSlug = (rawSlug: string | string[]): string => {
if (typeof rawSlug === 'string') {
return rawSlug
} else {
return rawSlug.join('/')
}
}
export default async function DownloadPage({
params,
}: {
params: { slug: string[] }
}): Promise<JSX.Element> {
const slug = normalizeSlug(params.slug)
const channel = await channelRepo.fetch(slug)
if (!channel) {
return (
<div className="flex flex-col items-center space-y-5 py-10 max-w-2xl mx-auto">
<Spinner direction="down" />
<Wordmark />
<p>Not found</p>
</div>
)
}
return (
<div className="flex flex-col items-center space-y-5 py-10 max-w-2xl mx-auto">
<Spinner direction="down" />
<Wordmark />
<WebRTCProvider>
<Downloader uploaderPeerID={channel.uploaderPeerID} />
</WebRTCProvider>
</div>
)
}

@ -0,0 +1,34 @@
import React from 'react'
import Footer from '../components/Footer'
import '../styles.css'
export const metadata = {
title: 'FilePizza • Your files, delivered.',
description: 'Peer-to-peer file transfers in your web browser.',
charSet: 'utf-8',
viewport: 'width=device-width, initial-scale=1',
openGraph: {
url: 'https://file.pizza',
title: 'FilePizza • Your files, delivered.',
description: 'Peer-to-peer file transfers in your web browser.',
images: [{ url: 'https://file.pizza/images/fb.png' }],
},
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}): React.ReactElement {
return (
<html lang="en">
<head>
<meta name="monetization" content="$twitter.xrptipbot.com/kernio" />
</head>
<body>
<main>{children}</main>
<Footer />
</body>
</html>
)
}

@ -1,3 +1,5 @@
'use client'
import React, { useCallback, useState } from 'react'
import WebRTCProvider from '../components/WebRTCProvider'
import DropZone from '../components/DropZone'
@ -7,12 +9,11 @@ import PasswordField from '../components/PasswordField'
import StartButton from '../components/StartButton'
import StopButton from '../components/StopButton'
import { UploadedFile } from '../types'
import { NextPage } from 'next'
import Spinner from '../components/Spinner'
import Wordmark from '../components/Wordmark'
import CancelButton from '../components/CancelButton'
export const IndexPage: NextPage = () => {
export default function IndexPage(): JSX.Element {
const [uploadedFiles, setUploadedFiles] = useState<UploadedFile[]>([])
const [password, setPassword] = useState('')
const [uploading, setUploading] = useState(false)
@ -44,10 +45,10 @@ export const IndexPage: NextPage = () => {
if (!uploadedFiles.length) {
return (
<div className="flex flex-col items-center space-y-5 py-10">
<div className="flex flex-col items-center space-y-5 py-10 max-w-2xl mx-auto">
<Spinner direction="up" />
<Wordmark />
<div className="flex flex-col items-center space-y-1">
<div className="flex flex-col items-center space-y-1 max-w-md">
<p className="text-lg text-center text-stone-800">
Peer-to-peer file transfers in your browser.
</p>
@ -62,11 +63,12 @@ export const IndexPage: NextPage = () => {
if (!uploading) {
return (
<div className="flex flex-col items-center space-y-5 py-10">
<div className="flex flex-col items-center space-y-5 py-10 max-w-2xl mx-auto">
<Spinner direction="up" />
<Wordmark />
<p className="text-lg text-center text-stone-800">
You are about to start uploading {uploadedFiles.length} files.
<p className="text-lg text-center text-stone-800 max-w-md">
You are about to start uploading {uploadedFiles.length}{' '}
{uploadedFiles.length === 1 ? 'file' : 'files'}.
</p>
<UploadFileList files={uploadedFiles} onChange={handleFileListChange} />
<PasswordField value={password} onChange={handleChangePassword} />
@ -79,11 +81,12 @@ export const IndexPage: NextPage = () => {
}
return (
<div className="flex flex-col items-center space-y-5 py-10">
<div className="flex flex-col items-center space-y-5 py-10 max-w-2xl mx-auto">
<Spinner direction="up" isRotating />
<Wordmark />
<p className="text-lg text-center text-stone-800">
You are uploading {uploadedFiles.length} files.
<p className="text-lg text-center text-stone-800 max-w-md">
You are uploading {uploadedFiles.length}{' '}
{uploadedFiles.length === 1 ? 'file' : 'files'}.
</p>
<UploadFileList files={uploadedFiles} />
<WebRTCProvider>
@ -93,9 +96,3 @@ export const IndexPage: NextPage = () => {
</div>
)
}
IndexPage.getInitialProps = () => {
return {}
}
export default IndexPage

@ -1,3 +1,4 @@
import 'server-only'
import config from './config'
import Redis from 'ioredis'
import { generateShortSlug, generateLongSlug } from './slugs'

@ -1,10 +1,10 @@
import React from 'react'
type Props = {
export default function CancelButton({
onClick,
}: {
onClick: React.MouseEventHandler
}
const CancelButton: React.FC<Props> = ({ onClick }: Props) => {
}): JSX.Element {
return (
<button
onClick={onClick}
@ -14,5 +14,3 @@ const CancelButton: React.FC<Props> = ({ onClick }: Props) => {
</button>
)
}
export default CancelButton

@ -1,10 +1,10 @@
import React from 'react'
type Props = {
export default function DownloadButton({
onClick,
}: {
onClick?: React.MouseEventHandler
}
const DownloadButton: React.FC<Props> = ({ onClick }: Props) => {
}): JSX.Element {
return (
<button
onClick={onClick}
@ -14,5 +14,3 @@ const DownloadButton: React.FC<Props> = ({ onClick }: Props) => {
</button>
)
}
export default DownloadButton

@ -1,3 +1,5 @@
'use client'
import React, { useCallback, useEffect, useRef, useState } from 'react'
import { useWebRTC } from './WebRTCProvider'
import {
@ -23,11 +25,12 @@ import ProgressBar from './ProgressBar'
const baseURL = process.env.NEXT_PUBLIC_BASE_URL ?? 'http://localhost:3000'
// eslint-disable-next-line @typescript-eslint/no-var-requires
if (process.browser) require('web-streams-polyfill/ponyfill')
if (typeof window !== 'undefined') require('web-streams-polyfill/ponyfill')
// eslint-disable-next-line @typescript-eslint/no-var-requires
const streamSaver = process.browser ? require('streamsaver') : null
if (process.browser) {
const streamSaver =
typeof window !== 'undefined' ? require('streamsaver') : null
if (typeof window !== 'undefined') {
streamSaver.mitm = baseURL + '/stream.html'
}

@ -1,21 +1,28 @@
'use client'
import React, { useCallback } from 'react'
const DONATE_HREF =
'https://commerce.coinbase.com/checkout/247b6ffe-fb4e-47a8-9a76-e6b7ef83ea22'
const FooterLink: React.FC<{ href: string; children: React.ReactNode }> = ({
function FooterLink({
href,
children,
}) => (
<a
className="text-stone-600 underline hover:text-stone-800"
href={href}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
)
}: {
href: string
children: React.ReactNode
}): JSX.Element {
return (
<a
className="text-stone-600 underline hover:text-stone-800"
href={href}
target="_blank"
rel="noopener noreferrer"
>
{children}
</a>
)
}
export function Footer(): JSX.Element {
const handleDonate = useCallback(() => {
@ -23,34 +30,38 @@ export function Footer(): JSX.Element {
}, [])
return (
<footer className="text-center py-2.5 pb-4 text-xs border-t border-stone-200 shadow-[0_-1px_2px_rgba(0,0,0,0.04)]">
<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="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">
<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">
<strong>Like FilePizza?</strong> Support its development!{' '}
</p>
<button
className="px-1.5 py-0.5 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors duration-200 font-medium text-[10px]"
onClick={handleDonate}
>
Donate
</button>
</div>
<p className="text-stone-600">
<strong>Like FilePizza?</strong> Support its development!{' '}
Cooked up by{' '}
<FooterLink href="http://kern.io">Alex Kern</FooterLink> &amp;{' '}
<FooterLink href="http://neeraj.io">Neeraj Baid</FooterLink> while
eating <strong>Sliver</strong> @ UC Berkeley &middot;{' '}
<FooterLink href="https://github.com/kern/filepizza#faq">
FAQ
</FooterLink>{' '}
&middot;{' '}
<FooterLink href="https://github.com/kern/filepizza">
Fork us
</FooterLink>
</p>
<button
className="px-1.5 py-0.5 bg-green-500 text-white rounded-md hover:bg-green-600 transition-colors duration-200 font-medium text-[10px]"
onClick={handleDonate}
>
Donate
</button>
</div>
<p className="text-stone-600">
Cooked up by <FooterLink href="http://kern.io">Alex Kern</FooterLink>{' '}
&amp; <FooterLink href="http://neeraj.io">Neeraj Baid</FooterLink>{' '}
while eating <strong>Sliver</strong> @ UC Berkeley &middot;{' '}
<FooterLink href="https://github.com/kern/filepizza#faq">
FAQ
</FooterLink>{' '}
&middot;{' '}
<FooterLink href="https://github.com/kern/filepizza">
Fork us
</FooterLink>
</p>
</div>
</footer>
</footer>
</>
)
}

@ -1,21 +1,16 @@
import React, { useCallback } from 'react'
export function PasswordField({
value,
onChange,
isRequired,
isInvalid,
}: {
export default function PasswordField(props: {
value: string
onChange: (v: string) => void
isRequired?: boolean
isInvalid?: boolean
}): JSX.Element {
const handleChange = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
onChange(e.target.value)
function (e: React.ChangeEvent<HTMLInputElement>): void {
props.onChange(e.target.value)
},
[onChange],
[props.onChange],
)
return (
@ -23,15 +18,13 @@ export function PasswordField({
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-gray-300'
props.isInvalid ? 'border-red-500' : 'border-gray-300'
}`}
placeholder={
isRequired ? 'Enter password...' : 'Add password (optional)...'
props.isRequired ? 'Enter password...' : 'Add password (optional)...'
}
value={value}
value={props.value}
onChange={handleChange}
/>
)
}
export default PasswordField

@ -17,7 +17,7 @@ export default function ProgressBar({
isComplete ? 'bg-green-500' : 'bg-blue-500'
} transition-all duration-300 ease-in-out`}
style={{ width: `${percentage}%` }}
></div>
/>
</div>
)
}

@ -1,10 +1,10 @@
import React from 'react'
export function StartButton({
export default function StartButton({
onClick,
}: {
onClick: React.MouseEventHandler
}): JSX.Element {
onClick: React.MouseEventHandler<HTMLButtonElement>
}): React.ReactElement {
return (
<button
onClick={onClick}
@ -14,5 +14,3 @@ export function StartButton({
</button>
)
}
export default StartButton

@ -1,19 +1,26 @@
import React from 'react'
type Props = {
onClick: React.MouseEventHandler
export default function StopButton({
isDownloading,
onClick,
}: {
onClick: React.MouseEventHandler<HTMLButtonElement>
isDownloading?: boolean
}
const StopButton: React.FC<Props> = ({ isDownloading, onClick }: Props) => {
}): 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"
className="px-2 py-1 text-xs text-orange-500 bg-transparent hover:bg-orange-100 rounded transition-colors duration-200 flex items-center"
onClick={onClick}
>
<svg
className="w-4 h-4 mr-1"
viewBox="0 0 24 24"
fill="currentColor"
xmlns="http://www.w3.org/2000/svg"
>
<rect x="4" y="4" width="16" height="16" />
</svg>
{isDownloading ? 'Stop Download' : 'Stop Upload'}
</button>
)
}
export default StopButton

@ -0,0 +1,21 @@
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-gray-100 text-gray-800'
}
export default function TypeBadge({ type }: { type: string }): JSX.Element {
return (
<span
className={`px-2 py-1 text-xs font-semibold rounded-full ${getTypeColor(
type,
)} transition-all duration-300 mr-2`}
>
{type}
</span>
)
}

@ -1,10 +1,10 @@
import React from 'react'
type Props = {
onClick?: React.MouseEventHandler
}
const UnlockButton: React.FC<Props> = ({ onClick }: Props) => {
export default function UnlockButton({
onClick,
}: {
onClick?: React.MouseEventHandler<HTMLButtonElement>
}): React.ReactElement {
return (
<button
onClick={onClick}
@ -14,5 +14,3 @@ const UnlockButton: React.FC<Props> = ({ onClick }: Props) => {
</button>
)
}
export default UnlockButton

@ -1,4 +1,5 @@
import React from 'react'
import TypeBadge from './TypeBadge'
type UploadedFileLike = {
fullPath?: string
@ -6,40 +7,21 @@ type UploadedFileLike = {
type: string
}
interface Props {
files: UploadedFileLike[]
onChange?: (updatedFiles: UploadedFileLike[]) => void
}
const getFileName = (file: UploadedFileLike): string => {
function getFileName(file: UploadedFileLike): string {
if (file.fullPath) {
return file.fullPath.slice(1)
}
return file.name || 'Unknown'
}
export function TypeBadge({ type }: { type: string }): JSX.Element {
const 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-gray-100 text-gray-800'
}
return (
<span
className={`px-2 py-1 text-xs font-semibold rounded-full ${getTypeColor(
type,
)} transition-all duration-300 mr-2`}
>
{type}
</span>
)
}
export function UploadFileList({ files, onChange }: Props): JSX.Element {
const handleRemove = (index: number) => {
export default function UploadFileList({
files,
onChange,
}: {
files: UploadedFileLike[]
onChange?: (updatedFiles: UploadedFileLike[]) => void
}): JSX.Element {
function handleRemove(index: number): void {
if (onChange) {
const updatedFiles = files.filter((_, i) => i !== index)
onChange(updatedFiles)
@ -70,5 +52,3 @@ export function UploadFileList({ files, onChange }: Props): JSX.Element {
return <div className="w-full">{items}</div>
}
export default UploadFileList

@ -36,6 +36,7 @@ type UploaderConnection = {
// TODO(@kern): Use better values
const RENEW_INTERVAL = 5000 // 20 minutes
const MAX_CHUNK_SIZE = 10 * 1024 * 1024 // 10 Mi
const QR_CODE_SIZE = 128
function useUploaderChannel(uploaderPeerID: string): {
loading: boolean
@ -330,36 +331,46 @@ export default function Uploader({
return (
<>
<div className="flex w-full">
<div className="flex-none">
<QRCode value={shortURL} size={88} />
<div className="flex w-full items-center">
<div className="flex-none mr-4">
<QRCode value={shortURL} size={QR_CODE_SIZE} />
</div>
<div className="flex-auto flex flex-col">
<div className="flex w-full">
<input
className="flex-grow px-2 py-1 text-xs border rounded-l"
value={longURL}
readOnly
/>
<button
className="px-4 py-1 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-r"
onClick={onCopyLongURL}
>
{hasCopiedLongURL ? 'Copied' : 'Copy'}
</button>
<div className="flex-auto flex flex-col justify-center space-y-2">
<div className="flex flex-col w-full">
<label className="text-[10px] text-gray-400 mb-0.5 font-bold">
Long URL
</label>
<div className="flex w-full">
<input
className="flex-grow px-3 py-2 text-xs border border-r-0 rounded-l"
value={longURL}
readOnly
/>
<button
className="px-4 py-2 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-r border-t border-r border-b"
onClick={onCopyLongURL}
>
{hasCopiedLongURL ? 'Copied' : 'Copy'}
</button>
</div>
</div>
<div className="flex w-full mt-2">
<input
className="flex-grow px-2 py-1 text-xs border rounded-l"
value={shortURL}
readOnly
/>
<button
className="px-4 py-1 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-r"
onClick={onCopyShortURL}
>
{hasCopiedShortURL ? 'Copied' : 'Copy'}
</button>
<div className="flex flex-col w-full mt-2">
<label className="text-[10px] text-gray-400 mb-0.5 font-bold">
Short URL
</label>
<div className="flex w-full">
<input
className="flex-grow px-3 py-2 text-xs border border-r-0 rounded-l"
value={shortURL}
readOnly
/>
<button
className="px-4 py-2 text-sm text-gray-700 bg-gray-100 hover:bg-gray-200 rounded-r border-t border-r border-b"
onClick={onCopyShortURL}
>
{hasCopiedShortURL ? 'Copied' : 'Copy'}
</button>
</div>
</div>
</div>
</div>

@ -1,3 +1,5 @@
'use client'
import React, { useState, useEffect, useRef, useContext } from 'react'
import type { default as PeerType } from 'peerjs'
import Loading from './Loading'

@ -1,32 +0,0 @@
import React from 'react'
import { AppProps } from 'next/app'
import Head from 'next/head'
import Footer from '../components/Footer'
import '../styles.css'
const App: React.FC<AppProps> = ({ Component, pageProps }: AppProps) => (
<>
<Head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="monetization" content="$twitter.xrptipbot.com/kernio" />
<meta property="og:url" content="https://file.pizza" />
<meta property="og:title" content="FilePizza • Your files, delivered." />
<meta
property="og:description"
content="Peer-to-peer file transfers in your web browser."
/>
<meta property="og:image" content="https://file.pizza/images/fb.png" />
<title>FilePizza Your files, delivered.</title>
<meta property="og:title" content="FilePizza" key="title" />
</Head>
<main>
<Component {...pageProps} />
</main>
<Footer />
</>
)
export default App

@ -1,51 +0,0 @@
import React from 'react'
import WebRTCProvider from '../../components/WebRTCProvider'
import Downloader from '../../components/Downloader'
import { NextPage, GetServerSideProps } from 'next'
import { channelRepo } from '../../channel'
import Spinner from '../../components/Spinner'
import Wordmark from '../../components/Wordmark'
type Props = {
slug: string
uploaderPeerID: string
error?: string
}
const DownloadPage: NextPage<Props> = ({ uploaderPeerID }) => {
return (
<div className="flex flex-col items-center space-y-5 py-10 w-full">
<Spinner direction="down" />
<Wordmark />
<WebRTCProvider>
<Downloader uploaderPeerID={uploaderPeerID} />
</WebRTCProvider>
</div>
)
}
export const getServerSideProps: GetServerSideProps<Props> = async (ctx) => {
const slug = normalizeSlug(ctx.query.slug)
const channel = await channelRepo.fetch(slug)
if (!channel) {
ctx.res.statusCode = 404
return {
props: { slug, uploaderPeerID: '', error: 'not found' },
}
}
return {
props: { slug, uploaderPeerID: channel.uploaderPeerID },
}
}
const normalizeSlug = (rawSlug: string | string[]): string => {
if (typeof rawSlug === 'string') {
return rawSlug
} else {
return rawSlug.join('/')
}
}
export default DownloadPage

@ -1,6 +1,49 @@
import xkcdPassword from 'xkcd-password'
import 'server-only'
import crypto from 'crypto'
import config from './config'
/**
* Generates an array of random words from a given word list.
*
* @param wordList - An array of words to choose from.
* @param numWords - The number of words to generate.
* @returns A Promise that resolves to an array of randomly selected words.
*/
function generateRandomWords(
wordList: string[],
numWords: number,
): Promise<string[]> {
return new Promise((resolve, reject) => {
if (!Array.isArray(wordList) || wordList.length === 0) {
reject(new Error('Word list must be a non-empty array'))
return
}
if (numWords <= 0) {
reject(new Error('Number of words must be greater than zero'))
return
}
const getRandomInt = (max: number): number => {
const buffer = new Uint32Array(1)
if (typeof window !== 'undefined' && window.crypto) {
window.crypto.getRandomValues(buffer)
} else {
crypto.randomFillSync(buffer)
}
return buffer[0] % max
}
const result: string[] = []
for (let i = 0; i < numWords; i++) {
const randomIndex = getRandomInt(wordList.length)
result.push(wordList[randomIndex])
}
resolve(result)
})
}
export const generateShortSlug = (): string => {
let result = ''
for (let i = 0; i < config.shortSlug.numChars; i++) {
@ -12,14 +55,10 @@ export const generateShortSlug = (): string => {
return result
}
const longSlugGenerator = new xkcdPassword()
longSlugGenerator.initWithWordList(config.longSlug.words)
export const generateLongSlug = async (): Promise<string> => {
const parts = await longSlugGenerator.generate({
numWords: config.longSlug.numWords,
minLength: 1,
maxLength: 256,
})
const parts = await generateRandomWords(
config.longSlug.words,
config.longSlug.numWords,
)
return parts.join('/')
}

@ -18,16 +18,23 @@
"isolatedModules": true,
"downlevelIteration": true,
"jsx": "preserve",
"incremental": true
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"strictNullChecks": true
},
"include": [
"tailwind.config.js",
"next-env.d.ts",
"src/**/*.js",
"src/**/*.ts",
"src/**/*.tsx"
"src/**/*.tsx",
".next/types/**/*.ts"
],
"exclude": [
"node_modules"
]
}
}

Loading…
Cancel
Save