Skip to content

Commit ff5d465

Browse files
committed
feat[major]: support MPU on frontend. refac several components
1 parent 7554baa commit ff5d465

24 files changed

+548
-403
lines changed

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default tseslint.config(
2020
rules: {
2121
"no-unused-vars": "off",
2222
"@typescript-eslint/no-unused-vars": [
23-
"warn",
23+
"error",
2424
{
2525
argsIgnorePattern: "^_",
2626
varsIgnorePattern: "^_",

frontend/components/CopyWidget.tsx

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Button, ButtonProps } from "@heroui/react"
2+
import { useRef, useState } from "react"
3+
import { CopyIcon, CheckIcon } from "./icons.js"
4+
5+
interface CopyIconProps extends ButtonProps {
6+
getCopyContent: () => string
7+
}
8+
9+
export function CopyWidget({ className, getCopyContent, ...rest }: CopyIconProps) {
10+
const numOfIssuedCopies = useRef(0)
11+
const [hasIssuedCopies, setHasIssuedCopies] = useState<boolean>(false)
12+
const onCopy = () => {
13+
const content = getCopyContent()
14+
navigator.clipboard
15+
.writeText(content)
16+
.then(() => {
17+
numOfIssuedCopies.current = numOfIssuedCopies.current + 1
18+
setHasIssuedCopies(numOfIssuedCopies.current > 0)
19+
20+
setTimeout(() => {
21+
numOfIssuedCopies.current = numOfIssuedCopies.current - 1
22+
setHasIssuedCopies(numOfIssuedCopies.current > 0)
23+
}, 1000)
24+
})
25+
.catch(console.error)
26+
}
27+
28+
return (
29+
<Button isIconOnly aria-label="Copy" className={className} onPress={onCopy} {...rest}>
30+
{hasIssuedCopies ? <CheckIcon className="size-6" /> : <CopyIcon className="size-6" />}
31+
</Button>
32+
)
33+
}

frontend/components/ErrorModal.tsx

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,56 @@
1-
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader } from "@heroui/react"
2-
import React from "react"
1+
import { Button, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, ModalProps } from "@heroui/react"
2+
import React, { useState } from "react"
3+
import { ErrorWithTitle } from "../utils/utils.js"
34

45
export type ErrorState = {
56
title: string
67
content: string
78
isOpen: boolean
89
}
910

10-
type ErrorModalProps = {
11-
onDismiss: () => void
12-
state: ErrorState
13-
}
11+
type ErrorModalProps = Omit<ModalProps, "children">
12+
13+
export function useErrorModal() {
14+
const [errorState, setErrorState] = useState<ErrorState>({ isOpen: false, content: "", title: "" })
15+
16+
function showModal(title: string, content: string) {
17+
setErrorState({ title, content, isOpen: true })
18+
}
19+
20+
async function handleFailedResp(defaultTitle: string, resp: Response) {
21+
const statusText = resp.statusText === "error" ? "Unknown error" : resp.statusText
22+
const errText = (await resp.text()) || statusText
23+
showModal(defaultTitle, errText)
24+
}
25+
26+
function handleError(defaultTitle: string, error: Error) {
27+
if (error instanceof ErrorWithTitle) {
28+
showModal(error.title, error.message)
29+
} else {
30+
showModal(defaultTitle, error.message)
31+
}
32+
}
33+
34+
const ErrorModal = ({ ...rest }: ErrorModalProps) => {
35+
const onClose = () => {
36+
setErrorState({ isOpen: false, content: "", title: "" })
37+
}
38+
return (
39+
<Modal isOpen={errorState.isOpen} state={errorState} onClose={onClose} {...rest}>
40+
<ModalContent>
41+
<ModalHeader className="flex flex-col gap-1">{errorState.title}</ModalHeader>
42+
<ModalBody>
43+
<p>{errorState.content}</p>
44+
</ModalBody>
45+
<ModalFooter>
46+
<Button color="danger" variant="light" onPress={onClose}>
47+
Close
48+
</Button>
49+
</ModalFooter>
50+
</ModalContent>
51+
</Modal>
52+
)
53+
}
1454

15-
export function ErrorModal({ onDismiss, state }: ErrorModalProps) {
16-
return (
17-
<Modal
18-
isOpen={state.isOpen}
19-
onOpenChange={(open) => {
20-
if (!open) {
21-
onDismiss()
22-
}
23-
}}
24-
>
25-
<ModalContent>
26-
<ModalHeader className="flex flex-col gap-1">{state.title}</ModalHeader>
27-
<ModalBody>
28-
<p>{state.content}</p>
29-
</ModalBody>
30-
<ModalFooter>
31-
<Button color="danger" variant="light" onPress={() => onDismiss()}>
32-
Close
33-
</Button>
34-
</ModalFooter>
35-
</ModalContent>
36-
</Modal>
37-
)
55+
return { ErrorModal, showModal, errorState, handleError, handleFailedResp }
3856
}

frontend/components/PasteSettingPanel.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
CardProps,
66
Divider,
77
Input,
8+
mergeClasses,
89
Radio,
910
RadioGroup,
1011
Switch,
@@ -33,12 +34,13 @@ interface PasteSettingPanelProps extends CardProps {
3334
}
3435

3536
export function PanelSettingsPanel({ setting, onSettingChange, ...rest }: PasteSettingPanelProps) {
37+
const radioClassNames = mergeClasses(radioOverrides, { labelWrapper: "ml-2.5" })
3638
return (
3739
<Card aria-label="Pastebin setting panel" classNames={cardOverrides} {...rest}>
38-
<CardHeader className="text-2xl">Settings</CardHeader>
40+
<CardHeader className="text-2xl pl-4 pb-2">Settings</CardHeader>
3941
<Divider className={tst} />
4042
<CardBody>
41-
<div className="gap-4 mb-4 flex flex-row">
43+
<div className="gap-4 mb-3 flex flex-row">
4244
<Input
4345
type="text"
4446
label="Expiration"
@@ -68,32 +70,32 @@ export function PanelSettingsPanel({ setting, onSettingChange, ...rest }: PasteS
6870
/>
6971
</div>
7072
<RadioGroup
71-
className="gap-4 mb-2 w-full"
73+
className="gap-4 mb-3 w-full"
7274
value={setting.uploadKind}
7375
onValueChange={(v) => onSettingChange({ ...setting, uploadKind: v as UploadKind })}
7476
>
75-
<Radio value="short" description={`Example: ${BaseUrl}/BxWH`} classNames={radioOverrides}>
77+
<Radio value="short" description={`Example: ${BaseUrl}/BxWH`} classNames={radioClassNames}>
7678
Generate a short random URL
7779
</Radio>
7880
<Radio
7981
value="long"
8082
description={`Example: ${BaseUrl}/5HQWYNmjA4h44SmybeThXXAm`}
8183
classNames={{
8284
description: "text-ellipsis max-w-[calc(100vw-5rem)] whitespace-nowrap overflow-hidden",
83-
...radioOverrides,
85+
...radioClassNames,
8486
}}
8587
>
8688
Generate a long random URL
8789
</Radio>
88-
<Radio value="custom" classNames={radioOverrides} description={`Example: ${BaseUrl}/~stocking`}>
90+
<Radio value="custom" classNames={radioClassNames} description={`Example: ${BaseUrl}/~stocking`}>
8991
Set by your own
9092
</Radio>
9193
{setting.uploadKind === "custom" ? (
9294
<Input
9395
value={setting.name}
9496
onValueChange={(n) => onSettingChange({ ...setting, name: n })}
9597
type="text"
96-
classNames={inputOverrides}
98+
classNames={radioClassNames}
9799
isInvalid={!verifyName(setting.name)[0]}
98100
errorMessage={verifyName(setting.name)[1]}
99101
startContent={
@@ -103,7 +105,7 @@ export function PanelSettingsPanel({ setting, onSettingChange, ...rest }: PasteS
103105
}
104106
/>
105107
) : null}
106-
<Radio value="manage" classNames={radioOverrides}>
108+
<Radio value="manage" classNames={radioClassNames}>
107109
<div className="">Update or delete</div>
108110
</Radio>
109111
{setting.uploadKind === "manage" ? (

frontend/components/UploadedPanel.tsx

Lines changed: 76 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
import { Card, CardBody, CardHeader, CardProps, Divider, mergeClasses, Skeleton, Snippet } from "@heroui/react"
21
import React from "react"
3-
import { PasteResponse } from "../../shared/interfaces.js"
2+
3+
import { Card, CardBody, CardHeader, CardProps, CircularProgress, Divider, Input, mergeClasses } from "@heroui/react"
4+
5+
import type { PasteResponse } from "../../shared/interfaces.js"
46
import { tst } from "../utils/overrides.js"
7+
import { CopyWidget } from "./CopyWidget.js"
58

69
interface UploadedPanelProps extends CardProps {
710
isLoading: boolean
8-
pasteResponse: PasteResponse | null
9-
encryptionKey: string | null
11+
loadingProgress?: number
12+
pasteResponse?: PasteResponse
13+
encryptionKey?: string
1014
}
1115

1216
const makeDecryptionUrl = (url: string, key: string) => {
@@ -15,62 +19,79 @@ const makeDecryptionUrl = (url: string, key: string) => {
1519
return urlParsed.toString() + "#" + key
1620
}
1721

18-
export function UploadedPanel({ isLoading, pasteResponse, className, encryptionKey, ...rest }: UploadedPanelProps) {
19-
const snippetClassNames = {
20-
pre: `overflow-scroll leading-[2.5] font-sans ${tst}`,
21-
base: `w-full py-1/3 ${tst}`,
22-
copyButton: `relative ml-[-12pt] left-[5pt] ${tst}`,
22+
export function UploadedPanel({
23+
isLoading,
24+
loadingProgress,
25+
pasteResponse,
26+
className,
27+
encryptionKey,
28+
...rest
29+
}: UploadedPanelProps) {
30+
const copyWidgetClassNames = `bg-transparent ${tst} translate-y-[10%]`
31+
const inputProps = {
32+
"aria-labelledby": "",
33+
readOnly: true,
34+
className: "mb-2",
2335
}
24-
const firstColClassNames = "w-[8rem] mr-4 whitespace-nowrap"
36+
2537
return (
2638
<Card classNames={mergeClasses({ base: tst }, { base: className })} {...rest}>
27-
<CardHeader className="text-2xl">Uploaded Paste</CardHeader>
39+
<CardHeader className="text-2xl pl-4 pb-2">Uploaded Paste</CardHeader>
2840
<Divider />
2941
<CardBody>
30-
<table className="border-spacing-2 border-separate table-fixed w-full">
31-
<tbody>
32-
<tr>
33-
<td className={firstColClassNames}>Paste URL</td>
34-
<td className="w-full">
35-
<Skeleton isLoaded={!isLoading} className="rounded-2xl grow">
36-
<Snippet hideSymbol variant="bordered" classNames={snippetClassNames}>
37-
{pasteResponse?.url}
38-
</Snippet>
39-
</Skeleton>
40-
</td>
41-
</tr>
42-
<tr>
43-
<td className={firstColClassNames}>Manage URL</td>
44-
<td className="w-full">
45-
<Skeleton isLoaded={!isLoading} className="rounded-2xl grow">
46-
<Snippet hideSymbol variant="bordered" classNames={snippetClassNames}>
47-
{pasteResponse?.manageUrl}
48-
</Snippet>
49-
</Skeleton>
50-
</td>
51-
</tr>
52-
{encryptionKey ? (
53-
<tr>
54-
<td className={firstColClassNames}>Decryption URL</td>
55-
<td className="w-full">
56-
<Skeleton isLoaded={!isLoading} className="rounded-2xl grow">
57-
<Snippet hideSymbol variant="bordered" classNames={snippetClassNames}>
58-
{pasteResponse && makeDecryptionUrl(pasteResponse.url, encryptionKey)}
59-
</Snippet>
60-
</Skeleton>
61-
</td>
62-
</tr>
63-
) : null}
64-
<tr>
65-
<td className={firstColClassNames}>Expire At</td>
66-
<td className="w-full py-2">
67-
<Skeleton isLoaded={!isLoading} className="rounded-2xl">
68-
{pasteResponse && new Date(pasteResponse.expireAt).toLocaleString()}
69-
</Skeleton>
70-
</td>
71-
</tr>
72-
</tbody>
73-
</table>
42+
{isLoading ? (
43+
<div className={"min-h-[5rem] w-full relative"}>
44+
<CircularProgress
45+
aria-label={"Loading..."}
46+
value={loadingProgress}
47+
className={"absolute top-[50%] left-[50%] translate-[-50%]"}
48+
/>
49+
</div>
50+
) : (
51+
pasteResponse && (
52+
<>
53+
{encryptionKey && (
54+
<Input
55+
{...inputProps}
56+
label={"Decryption URL"}
57+
color={"success"}
58+
value={makeDecryptionUrl(pasteResponse.url, encryptionKey)}
59+
endContent={
60+
<CopyWidget
61+
className={copyWidgetClassNames}
62+
getCopyContent={() => makeDecryptionUrl(pasteResponse.url, encryptionKey)}
63+
/>
64+
}
65+
/>
66+
)}
67+
<Input
68+
{...inputProps}
69+
label={"Paste URL"}
70+
value={pasteResponse.url}
71+
endContent={<CopyWidget className={copyWidgetClassNames} getCopyContent={() => pasteResponse.url} />}
72+
/>
73+
<Input
74+
{...inputProps}
75+
label={"Manage URL"}
76+
value={pasteResponse.manageUrl}
77+
endContent={
78+
<CopyWidget className={copyWidgetClassNames} getCopyContent={() => pasteResponse.manageUrl} />
79+
}
80+
/>
81+
{pasteResponse.suggestedUrl && (
82+
<Input
83+
{...inputProps}
84+
label={"Suggest URL"}
85+
value={pasteResponse.suggestedUrl}
86+
endContent={
87+
<CopyWidget className={copyWidgetClassNames} getCopyContent={() => pasteResponse.suggestedUrl!} />
88+
}
89+
/>
90+
)}
91+
<Input {...inputProps} label={"Expiration"} value={new Date(pasteResponse.expireAt).toLocaleString()} />
92+
</>
93+
)
94+
)}
7495
</CardBody>
7596
</Card>
7697
)

0 commit comments

Comments
 (0)