@@ -22,84 +22,38 @@ import {
2222 ModalContent ,
2323 ModalHeader ,
2424 ModalFooter ,
25+ Tooltip ,
2526} from "@heroui/react"
2627
28+ import { PasteResponse , parsePath , parseFilenameFromContentDisposition } from "../src/shared.js"
29+
2730import {
28- parseExpiration ,
29- PasteResponse ,
30- NAME_REGEX ,
31- PASSWD_SEP ,
32- parsePath ,
33- parseFilenameFromContentDisposition ,
34- parseExpirationReadable ,
35- } from "../src/shared .js"
31+ verifyExpiration ,
32+ verifyManageUrl ,
33+ verifyName ,
34+ formatSize ,
35+ maxExpirationReadable ,
36+ BaseUrl ,
37+ APIUrl ,
38+ } from "./utils .js"
3639
3740import "./style.css"
41+ import { computerIcon , moonIcon , sunIcon } from "./icons.js"
3842
39- function formatSize ( size : number ) : string {
40- if ( ! size ) return "0"
41- if ( size < 1024 ) {
42- return `${ size } Bytes`
43- } else if ( size < 1024 * 1024 ) {
44- return `${ ( size / 1024 ) . toFixed ( 2 ) } KB`
45- } else if ( size < 1024 * 1024 * 1024 ) {
46- return `${ ( size / 1024 / 1024 ) . toFixed ( 2 ) } MB`
47- } else {
48- return `${ ( size / 1024 / 1024 / 1024 ) . toFixed ( 2 ) } GB`
49- }
50- }
51-
52- const BaseUrl = DEPLOY_URL
53- const APIUrl = DEPLOY_URL || ""
54- const maxExpirationSeconds = parseExpiration ( MAX_EXPIRATION ) !
55- const maxExpirationReadable = parseExpirationReadable ( MAX_EXPIRATION ) !
56-
57- function verifyExpiration ( expiration : string ) : [ boolean , string ] {
58- const parsed = parseExpiration ( expiration )
59- if ( parsed === null ) {
60- return [ false , "Invalid expiration" ]
61- } else {
62- if ( parsed > maxExpirationSeconds ) {
63- return [ false , `Exceed max expiration (${ maxExpirationReadable } )` ]
64- } else {
65- return [ true , `Expires in ${ parseExpirationReadable ( expiration ) ! } ` ]
66- }
67- }
68- }
43+ type EditKind = "edit" | "file"
44+ type UploadKind = "short" | "long" | "custom" | "manage"
45+ type DarkMode = "dark" | "light" | "system"
6946
70- function verifyName ( name : string ) : [ boolean , string ] {
71- if ( name . length < 3 ) {
72- return [ false , "Should have at least 3 characters" ]
73- } else if ( ! NAME_REGEX . test ( name ) ) {
74- return [ false , "Should only contain alphanumeric and +_-[]*$@,;" ]
47+ function defaultDarkMode ( ) : DarkMode {
48+ const storedDarkModeSelect = localStorage . getItem ( "darkModeSelect" )
49+ if ( storedDarkModeSelect !== null && [ "light" , "dark" , "system" ] . includes ( storedDarkModeSelect ) ) {
50+ return storedDarkModeSelect as DarkMode
7551 } else {
76- return [ true , "" ]
77- }
78- }
79-
80- function verifyManageUrl ( url : string ) : [ boolean , string ] {
81- try {
82- const url_parsed = new URL ( url )
83- if ( url_parsed . origin !== BaseUrl ) {
84- return [ false , `URL should starts with ${ BaseUrl } ` ]
85- } else if ( url_parsed . pathname . indexOf ( PASSWD_SEP ) < 0 ) {
86- return [ false , `URL should contain a colon` ]
87- } else {
88- return [ true , "" ]
89- }
90- } catch ( e ) {
91- if ( e instanceof TypeError ) {
92- return [ false , "Invalid URL" ]
93- } else {
94- throw e
95- }
52+ return "system"
9653 }
9754}
9855
9956function PasteBin ( ) {
100- type EditKind = "edit" | "file"
101- type UploadKind = "short" | "long" | "custom" | "manage"
102-
10357 const [ editKind , setEditKind ] = useState < EditKind > ( "edit" )
10458 const [ pasteEdit , setPasteEdit ] = useState ( "" )
10559 const [ uploadFile , setUploadFile ] = useState < File | null > ( null )
@@ -117,6 +71,11 @@ function PasteBin() {
11771 const [ isModalOpen , setModalOpen ] = useState ( false )
11872 const [ modalErrMsg , setModalErrMsg ] = useState ( "" )
11973
74+ const [ darkModeSelect , setDarkModeSelect ] = useState < DarkMode > ( defaultDarkMode ( ) )
75+
76+ const systemDark = window . matchMedia ( "(prefers-color-scheme: dark)" ) . matches
77+ const isDark = darkModeSelect === "system" ? systemDark : darkModeSelect === "dark"
78+
12079 function showErrorMsg ( err : string ) {
12180 setModalErrMsg ( err )
12281 setModalOpen ( true )
@@ -153,6 +112,11 @@ function PasteBin() {
153112 </ Modal >
154113 )
155114
115+ useEffect ( ( ) => {
116+ localStorage . setItem ( "darkModeSelect" , darkModeSelect )
117+ } , [ darkModeSelect ] )
118+
119+ // handle admin URL
156120 useEffect ( ( ) => {
157121 const pathname = location . pathname
158122 const { nameFromPath, passwd, filename, ext } = parsePath ( pathname )
@@ -270,10 +234,34 @@ function PasteBin() {
270234 }
271235 }
272236
237+ const iconsMap = new Map ( [
238+ [ "system" , computerIcon ] ,
239+ [ "dark" , moonIcon ] ,
240+ [ "light" , sunIcon ] ,
241+ ] )
242+ const toggleDarkModeButton = (
243+ < Tooltip content = "Toggle Dark Mode" >
244+ < span
245+ className = "absolute right-0"
246+ onClick = { ( ) => {
247+ if ( darkModeSelect === "system" ) {
248+ setDarkModeSelect ( "dark" )
249+ } else if ( darkModeSelect === "dark" ) {
250+ setDarkModeSelect ( "light" )
251+ } else {
252+ setDarkModeSelect ( "system" )
253+ }
254+ } }
255+ >
256+ { iconsMap . get ( darkModeSelect ) }
257+ </ span >
258+ </ Tooltip >
259+ )
260+
273261 const info = (
274262 < div className = "mx-4 lg:mx-0" >
275- < h1 className = "text-3xl mt-8 mb-4" > Pastebin Worker</ h1 >
276- < p className = "my-2" > This is an open source pastebin deployed on Cloudflare Workers.</ p >
263+ < h1 className = "text-3xl mt-8 mb-4 relative " > Pastebin Worker { toggleDarkModeButton } </ h1 >
264+ < p className = "my-2" > This is an open source pastebin deployed on Cloudflare Workers. </ p >
277265 < p className = "my-2" >
278266 < b > Usage</ b > : paste any text here, submit, then share it with URL. (
279267 < Link href = { `${ BaseUrl } /api` } > API Documentation</ Link > )
@@ -534,7 +522,11 @@ function PasteBin() {
534522 )
535523
536524 return (
537- < div className = "flex flex-col items-center min-h-screen font-sans" >
525+ < div
526+ className = {
527+ "flex flex-col items-center min-h-screen font-sans bg-background text-foreground" + ( isDark ? " dark" : "" )
528+ }
529+ >
538530 < div className = "grow w-full max-w-[64rem]" >
539531 { info }
540532 { editor }
0 commit comments