Skip to content

Commit b411b1b

Browse files
committed
refac[frontend]: split many components from Pastebin
1 parent 00b620b commit b411b1b

File tree

6 files changed

+379
-314
lines changed

6 files changed

+379
-314
lines changed
Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { JSX } from "react"
1+
import React, { JSX, useEffect } from "react"
22
import { computerIcon, moonIcon, sunIcon } from "../icons.js"
33
import { Tooltip, TooltipProps } from "@heroui/react"
44

@@ -32,6 +32,10 @@ export function shouldBeDark(mode: DarkMode): boolean {
3232
}
3333

3434
export function DarkModeToggle({ mode, onModeChange, ...rest }: MyComponentProps) {
35+
useEffect(() => {
36+
localStorage.setItem("darkModeSelect", mode)
37+
}, [mode])
38+
3539
return (
3640
<Tooltip content="Toggle Dark Mode" {...rest}>
3741
<span

frontend/components/ErrorModal.tsx

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@heroui/react"
2+
import React from "react"
3+
4+
type ErrorModalProps = {
5+
onDismiss: () => void
6+
isOpen: boolean
7+
title: string
8+
content: string
9+
}
10+
11+
export function ErrorModal({ onDismiss, title, content, isOpen }: ErrorModalProps) {
12+
return (
13+
<Modal
14+
isOpen={isOpen}
15+
onOpenChange={(open) => {
16+
if (!open) {
17+
onDismiss()
18+
}
19+
}}
20+
>
21+
<ModalContent>
22+
<ModalHeader className="flex flex-col gap-1">{title}</ModalHeader>
23+
<ModalBody>
24+
<p>{content}</p>
25+
</ModalBody>
26+
<ModalFooter>
27+
<Button color="danger" variant="light" onPress={() => onDismiss()}>
28+
Close
29+
</Button>
30+
</ModalFooter>
31+
</ModalContent>
32+
</Modal>
33+
)
34+
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import { Button, Card, CardBody, CardProps, Tab, Tabs, Textarea } from "@heroui/react"
2+
import React from "react"
3+
import { formatSize } from "../utils.js"
4+
5+
export type EditKind = "edit" | "file"
6+
7+
export type PasteEditState = {
8+
editKind: EditKind
9+
editContent: string
10+
file: File | null
11+
}
12+
13+
interface PasteEditorProps extends CardProps {
14+
isPasteLoading: boolean
15+
state: PasteEditState
16+
onStateChange: (state: PasteEditState) => void
17+
}
18+
19+
function displayFileInfo(file: File | null) {
20+
if (file === null) {
21+
return null
22+
} else {
23+
return (
24+
<span className="ml-4">
25+
<code>{file.name}</code> ({formatSize(file.size)})
26+
</span>
27+
)
28+
}
29+
}
30+
31+
export function PasteEditor({ isPasteLoading, state, onStateChange, ...rest }: PasteEditorProps) {
32+
return (
33+
<Card {...rest}>
34+
<CardBody>
35+
<Tabs
36+
variant="underlined"
37+
classNames={{
38+
tabList: "ml-4 gap-6 w-full p-0 border-divider",
39+
cursor: "w-full",
40+
tab: "max-w-fit px-0 h-8",
41+
}}
42+
selectedKey={state.editKind}
43+
onSelectionChange={(k) => {
44+
onStateChange({ ...state, editKind: k as EditKind })
45+
}}
46+
>
47+
<Tab key={"edit"} title="Edit">
48+
<Textarea
49+
isClearable
50+
data-testid="pastebin-edit"
51+
placeholder={isPasteLoading ? "Loading..." : "Edit your paste here"}
52+
isDisabled={isPasteLoading}
53+
className="px-0 py-0"
54+
classNames={{
55+
input: "resize-y min-h-[30em] font-mono",
56+
}}
57+
name="c"
58+
disableAutosize
59+
disableAnimation
60+
value={state.editContent}
61+
onValueChange={(k) => {
62+
onStateChange({ ...state, editContent: k })
63+
}}
64+
variant="faded"
65+
isRequired
66+
></Textarea>
67+
</Tab>
68+
<Tab key="file" title="File">
69+
<Button radius="sm" color="primary" as="label">
70+
<input
71+
type="file"
72+
className="w-0 h-0 overflow-hidden absolute inline"
73+
onChange={(event) => {
74+
const files = event.target.files
75+
if (files && files.length) {
76+
onStateChange({
77+
...state,
78+
editKind: "file",
79+
file: files[0],
80+
})
81+
}
82+
}}
83+
/>
84+
Upload
85+
</Button>
86+
{displayFileInfo(state.file)}
87+
</Tab>
88+
</Tabs>
89+
</CardBody>
90+
</Card>
91+
)
92+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { Card, CardBody, CardHeader, CardProps, Divider, Input, Radio, RadioGroup } from "@heroui/react"
2+
import { BaseUrl, verifyExpiration, verifyManageUrl, verifyName } from "../utils.js"
3+
import React from "react"
4+
5+
export type UploadKind = "short" | "long" | "custom" | "manage"
6+
7+
export type PasteSetting = {
8+
uploadKind: UploadKind
9+
expiration: string
10+
password: string
11+
name: string
12+
manageUrl: string
13+
}
14+
15+
interface PasteSettingPanelProps extends CardProps {
16+
setting: PasteSetting
17+
onSettingChange: (setting: PasteSetting) => void
18+
}
19+
20+
export function PanelSettingsPanel({ setting, onSettingChange, ...rest }: PasteSettingPanelProps) {
21+
return (
22+
<Card {...rest}>
23+
<CardHeader className="text-2xl">Settings</CardHeader>
24+
<Divider />
25+
<CardBody>
26+
<div className="gap-4 mb-6 flex flex-row">
27+
<Input
28+
type="text"
29+
label="Expiration"
30+
className="basis-80"
31+
defaultValue="7d"
32+
value={setting.expiration}
33+
isRequired
34+
onValueChange={(e) => onSettingChange({ ...setting, expiration: e })}
35+
isInvalid={!verifyExpiration(setting.expiration)[0]}
36+
errorMessage={verifyExpiration(setting.expiration)[1]}
37+
description={verifyExpiration(setting.expiration)[1]}
38+
/>
39+
<Input
40+
type="password"
41+
label="Password"
42+
value={setting.password}
43+
onValueChange={(p) => onSettingChange({ ...setting, password: p })}
44+
placeholder={"Generated randomly"}
45+
description="Used to update/delete your paste"
46+
/>
47+
</div>
48+
<RadioGroup
49+
className="gap-4 mb-2 w-full"
50+
value={setting.uploadKind}
51+
onValueChange={(v) => onSettingChange({ ...setting, uploadKind: v as UploadKind })}
52+
>
53+
<Radio value="short" description={`Example: ${BaseUrl}/BxWH`}>
54+
Generate a short random URL
55+
</Radio>
56+
<Radio
57+
value="long"
58+
description={`Example: ${BaseUrl}/5HQWYNmjA4h44SmybeThXXAm`}
59+
classNames={{
60+
description: "text-ellipsis max-w-[calc(100vw-5rem)] whitespace-nowrap overflow-hidden",
61+
}}
62+
>
63+
Generate a long random URL
64+
</Radio>
65+
<Radio value="custom" description={`Example: ${BaseUrl}/~stocking`}>
66+
Set by your own
67+
</Radio>
68+
{setting.uploadKind === "custom" ? (
69+
<Input
70+
value={setting.name}
71+
onValueChange={(n) => onSettingChange({ ...setting, name: n })}
72+
type="text"
73+
className="shrink"
74+
isInvalid={!verifyName(setting.name)[0]}
75+
errorMessage={verifyName(setting.name)[1]}
76+
startContent={
77+
<div className="pointer-events-none flex items-center">
78+
<span className="text-default-500 text-small w-max">{`${BaseUrl}/~`}</span>
79+
</div>
80+
}
81+
/>
82+
) : null}
83+
<Radio value="manage">
84+
<div className="">Update or delete</div>
85+
</Radio>
86+
{setting.uploadKind === "manage" ? (
87+
<Input
88+
value={setting.manageUrl}
89+
onValueChange={(m) => onSettingChange({ ...setting, manageUrl: m })}
90+
type="text"
91+
className="shrink"
92+
isInvalid={!verifyManageUrl(setting.manageUrl)[0]}
93+
errorMessage={verifyManageUrl(setting.manageUrl)[1]}
94+
placeholder={`Manage URL`}
95+
/>
96+
) : null}
97+
</RadioGroup>
98+
</CardBody>
99+
</Card>
100+
)
101+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Card, CardBody, CardHeader, CardProps, Divider, Skeleton, Snippet } from "@heroui/react"
2+
import React from "react"
3+
import { PasteResponse } from "../../src/shared.js"
4+
5+
interface UploadedPanelProps extends CardProps {
6+
pasteResponse: PasteResponse | null
7+
}
8+
9+
export function UploadedPanel({ pasteResponse, ...rest }: UploadedPanelProps) {
10+
const snippetClassNames = {
11+
pre: "overflow-scroll leading-[2.5]",
12+
base: "w-full py-2/3",
13+
copyButton: "relative ml-[-12pt] left-[5pt]",
14+
}
15+
const firstColClassNames = "w-[7rem] whitespace-nowrap"
16+
return (
17+
<Card {...rest}>
18+
<CardHeader className="text-2xl">Uploaded Paste</CardHeader>
19+
<Divider />
20+
<CardBody>
21+
<table className="border-spacing-2 border-separate table-fixed w-full">
22+
<tbody>
23+
<tr>
24+
<td className={firstColClassNames}>Paste URL</td>
25+
<td className="w-full">
26+
<Skeleton isLoaded={pasteResponse !== null} className="rounded-2xl grow">
27+
<Snippet hideSymbol variant="bordered" classNames={snippetClassNames}>
28+
{pasteResponse?.url}
29+
</Snippet>
30+
</Skeleton>
31+
</td>
32+
</tr>
33+
<tr>
34+
<td className={firstColClassNames}>Manage URL</td>
35+
<td className="w-full overflow-hidden">
36+
<Skeleton isLoaded={pasteResponse !== null} className="rounded-2xl grow">
37+
<Snippet hideSymbol variant="bordered" classNames={snippetClassNames}>
38+
{pasteResponse?.manageUrl}
39+
</Snippet>
40+
</Skeleton>
41+
</td>
42+
</tr>
43+
{pasteResponse?.suggestedUrl ? (
44+
<tr>
45+
<td className={firstColClassNames}>Suggested URL</td>
46+
<td className="w-full">
47+
<Snippet hideSymbol variant="bordered" classNames={snippetClassNames}>
48+
{pasteResponse?.suggestedUrl}
49+
</Snippet>
50+
</td>
51+
</tr>
52+
) : null}
53+
<tr>
54+
<td className={firstColClassNames}>Expire At</td>
55+
<td className="w-full py-2">
56+
<Skeleton isLoaded={pasteResponse !== null} className="rounded-2xl">
57+
{pasteResponse && new Date(pasteResponse.expireAt).toLocaleString()}
58+
</Skeleton>
59+
</td>
60+
</tr>
61+
</tbody>
62+
</table>
63+
</CardBody>
64+
</Card>
65+
)
66+
}

0 commit comments

Comments
 (0)