Skip to content

Commit

Permalink
feat: improve form & loading states
Browse files Browse the repository at this point in the history
  • Loading branch information
GalvinGao committed Aug 24, 2023
1 parent 1073556 commit 4036ef3
Show file tree
Hide file tree
Showing 11 changed files with 288 additions and 28 deletions.
13 changes: 13 additions & 0 deletions src/components/Card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Card } from "@mui/material"
import { FC, PropsWithChildren } from "react"

export const EmptyStateCard: FC<PropsWithChildren> = ({ children }) => {
return (
<Card
className="p-4 shadow-lg min-h-[40rem] h-full flex flex-col gap-2 items-center justify-center"
key="empty"
>
{children}
</Card>
)
}
33 changes: 22 additions & 11 deletions src/components/Tegami.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,31 @@
import clsx from "clsx"
import { FC, PropsWithChildren } from "react"

export const Footer: FC<{ className?: string }> = ({ className }) => {
return (
<div
className={clsx(
"flex items-center font-typing0 gap-2 select-none",
className,
)}
>
<img
src="https://penguin.upyun.galvincdn.com/logos/penguin_stats_logo.png"
alt="logo"
className="h-8"
/>
<div className="leading-none translate-y-1">
A Penguin Statistics Project
</div>
</div>
)
}

export const WhiteRootLayout: FC<PropsWithChildren> = ({ children }) => {
return (
<div className="p-40 h-full w-full flex justify-center items-center">
{children}
<div className="absolute bottom-12 flex items-center font-typing0 gap-2 select-none">
<img
src="https://penguin.upyun.galvincdn.com/logos/penguin_stats_logo.png"
alt="logo"
className="h-8"
/>
<div className="leading-none translate-y-1">
A Penguin Statistics Project
</div>
</div>
<Footer className="absolute bottom-12" />
</div>
)
}
Expand All @@ -29,7 +40,7 @@ export const Cover: FC<
style={{
backgroundImage: `url(/images/card.png)`,
backgroundSize: "cover",

// Magic number by measuring the actual element size of 1920x1080 screen
height: 760,
width: 982.3,
Expand Down
43 changes: 41 additions & 2 deletions src/components/rjsf/Form.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,45 @@
import { withTheme } from "@rjsf/core"
import { Tooltip } from "@mui/material"
import { FormProps, withTheme } from "@rjsf/core"
import { Theme } from "@rjsf/mui"
import { customizeValidator } from "@rjsf/validator-ajv8"
import { localizeChineseErrorMessage } from "./errorFormatter"

// Make modifications to the theme with your own fields and widgets

export const Form = withTheme(Theme)
const ThemedForm = withTheme({
...Theme,
templates: {
...Theme.templates,
// Add custom templates here
ErrorListTemplate: ({ errors }) => {
return (
<div className="bg-red-100 p-4 my-2 shadow">
<h4 className="font-bold text-lg">有些内容看起来不太对...</h4>
<ul className="list-disc pl-4 mt-1">
{errors.map((error, i) => (
<Tooltip
title={
<div className="whitespace-pre-wrap font-mono">
{JSON.stringify(error, null, 2)}
</div>
}
key={i}
placement="bottom-start"
>
<li className="cursor-help">{error.stack}</li>
</Tooltip>
))}
</ul>
</div>
)
},
},
})

const validator = customizeValidator({}, localizeChineseErrorMessage)

export function Form(props: Omit<FormProps, "validator" | "liveValidate">) {
return (
<ThemedForm liveValidate validator={validator} noHtml5Validate {...props} />
)
}
136 changes: 136 additions & 0 deletions src/components/rjsf/errorFormatter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import { Localizer } from "@rjsf/validator-ajv8"

export const localizeChineseErrorMessage: Localizer = errors => {
if (!(errors && errors.length)) return
for (const e of errors) {
let out = ""
switch (e.keyword) {
case "additionalItems":
case "items":
out = `不允许超过 ${e.params.limit} 个元素`
break
case "additionalProperties":
out = "不允许有额外的属性"
break
case "anyOf":
out = "数据应为 anyOf 所指定的其中一个"
break
case "const":
out = "应当等于常量"
break
case "contains":
out = "应当包含一个有效项"
break
case "dependencies":
case "dependentRequired":
out += "应当拥有属性" + e.params.property + "的依赖属性" + e.params.deps
break
case "discriminator":
switch (e.params.error) {
case "tag":
out = '标签 "' + e.params.tag + '" 的类型必须为字符串'
break
case "mapping":
out = '标签 "' + e.params.tag + '" 的值必须在 oneOf 之中'
break
default:
out = '应当通过 "' + e.keyword + ' 关键词校验"'
}
break
case "enum":
out = "应当是预设定的枚举值之一"
break
case "false schema":
out = "布尔模式出错"
break
case "format":
out = '应当匹配格式 "' + e.params.format + '"'
break
// case "formatMaximum":
// out = `不应当大于 ${e.params.limit} (应当 ${e.params.comparison} ${e.params.limit})`
// break
// case "formatExclusiveMaximum":
// out = ""
// const cond = e.params.comparison + " " + e.params.limit
// out += "应当是 " + cond
// break
// case "formatMinimum":
// case "formatExclusiveMinimum":
// out = ""
// const cond = e.params.comparison + " " + e.params.limit
// out += "应当是 " + cond
// break
case "if":
out = '应当匹配模式 "' + e.params.failingKeyword + '" '
break
case "maxItems":
out = `应当仅有最多 ${e.params.limit} 项`
break
case "maxLength":
out = `应当小于或等于 ${e.params.limit} 个字符`
break
case "minimum":
out = `应当大于或等于 ${e.params.limit}`
break
case "exclusiveMinimum":
out = `应当大于 (且不等于) ${e.params.limit}`
break
case "maximum":
out = `应当小于或等于 ${e.params.limit}`
break
case "exclusiveMaximum":
out = `应当小于 (且不等于) ${e.params.limit}`
break
case "minItems":
out = `最少应当有 ${e.params.limit} 个项`
break
case "minLength":
out = `应当大于或等于 ${e.params.limit} 个字符`
break
case "minProperties":
out += `应当至少有 ${e.params.limit} 个属性`
break
case "multipleOf":
out = "应当是 " + e.params.multipleOf + " 的整数倍"
break
case "not":
out = '不应当匹配 "not" schema'
break
case "oneOf":
out = '只能匹配一个 "oneOf" 中的 schema'
break
case "pattern":
out = '应当匹配正则表达式 "' + e.params.pattern + '"'
break
case "patternRequired":
out = '应当有属性匹配正则表达式 "' + e.params.missingPattern + '"'
break
case "propertyNames":
out = "属性名无效"
break
case "required":
out = "应当有必需属性 " + e.params.missingProperty
break
case "type":
out = "应当是 " + e.params.type + " 类型"
break
case "unevaluatedItems":
out += ` 不允许有超过 ${e.params.len} 个元素`
break
case "unevaluatedProperties":
out = "不允许存在未求值的属性"
break
case "uniqueItems":
out =
"不应当含有重复项 (第 " +
e.params.j +
" 项与第 " +
e.params.i +
" 项是重复的)"
break
default:
out = '应当通过 "' + e.keyword + ' 关键词校验"'
}
e.message = out
}
}
25 changes: 19 additions & 6 deletions src/layouts/RootLayout.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { AccountCircle, Logout } from "@mui/icons-material"
import {
AppBar,
CircularProgress,
Container,
IconButton,
ListItemIcon,
Expand All @@ -10,11 +11,12 @@ import {
Tooltip,
Typography,
} from "@mui/material"
import { FC, useState } from "react"
import { FC, Suspense, useState } from "react"
import { toast } from "react-hot-toast"
import { graphql, useLazyLoadQuery } from "react-relay"
import { Outlet, useNavigate } from "react-router-dom"
import { useEffectOnce } from "react-use"
import { Footer } from "../components/Tegami"
import { getToken, setToken, useToken } from "../utils/storage"
import { RootLayoutQuery } from "./__generated__/RootLayoutQuery.graphql"

Expand All @@ -39,23 +41,34 @@ export const RootLayout: FC = () => {
alt="logo"
className="h-8 mr-2"
/>
<Typography variant="h6" component="div">
<Typography variant="h6" component="div" className="select-none">
RogueStats
<span className="font-light">&nbsp;Console</span>
</Typography>
<Tooltip
title="构建版本"
arrow
className="bg-slate-800 hover:bg-slate-700 text-white text-xs px-2 py-1 cursor-help"
>
{import.meta.env.VITE_BUILD_GIT_COMMIT || "未知构建"}
<div>{import.meta.env.VITE_BUILD_GIT_COMMIT || "未知构建"}</div>
</Tooltip>
<div className="flex-1" />
{token && <AccountButton />}
<Suspense
fallback={
<CircularProgress color="inherit" size={24} className="mr-3" />
}
>
{token && <AccountButton />}
</Suspense>
</Toolbar>
</AppBar>
<Container maxWidth="lg" className="pt-24">

<Container maxWidth="lg" className="py-24 h-full">
<Outlet />

<div className="w-full flex items-center jcustify-enter py-24">
<Footer />
</div>
</Container>
</>
)
Expand Down
13 changes: 13 additions & 0 deletions src/pages/research/ResearchDetailEmptyPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { FC } from "react"
import { EmptyStateCard } from "../../components/Card"

export const ResearchDetailEmptyPage: FC = () => {
return (
<EmptyStateCard>
<h1 className="text-lg text-slate-700 select-none">暂未选定任何课题</h1>
<p className="text-slate-500 text-sm select-none">
于左侧选择一个课题以继续
</p>
</EmptyStateCard>
)
}
5 changes: 2 additions & 3 deletions src/pages/research/ResearchDetailPage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Card } from "@mui/material"
import validator from "@rjsf/validator-ajv8"
import { FC } from "react"
import { graphql, useLazyLoadQuery } from "react-relay"
import { useParams } from "react-router-dom"
Expand All @@ -24,9 +23,9 @@ export const ResearchDetailPage: FC = () => {
)

return (
<Card className="p-4 shadow-lg">
<Card className="p-4 shadow-lg" key={data.research.id}>
<h1 className="text-lg">{data.research.name}</h1>
<Form schema={data.research.schema} validator={validator} />
<Form schema={data.research.schema} />
</Card>
)
}
23 changes: 19 additions & 4 deletions src/pages/research/ResearchIndexPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ import clsx from "clsx"
import { Suspense } from "react"
import { graphql, useLazyLoadQuery } from "react-relay"
import { Link, Outlet, useMatch } from "react-router-dom"
import { EmptyStateCard } from "../../components/Card"
import { withSuspensible } from "../../utils/suspensible"
import { ResearchIndexPageQuery } from "./__generated__/ResearchIndexPageQuery.graphql"

export const ResearchIndexPage = () => {
export const ResearchIndexPage = withSuspensible(() => {
const matches = useMatch("/research/:id")
const data = useLazyLoadQuery<ResearchIndexPageQuery>(
graphql`
Expand All @@ -32,11 +34,18 @@ export const ResearchIndexPage = () => {
<div className="flex items-start gap-4">
<div className="w-48 flex flex-col gap-2">
{data.researches.map(research => (
<Link to={`/research/${research.id}`} key={research.id}>
<Link
to={`/research/${research.id}`}
key={research.id}
className={clsx(
matches?.params.id === research.id && "cursor-default",
)}
>
<ButtonBase
key={research.id}
color="transparent"
className="shadow-lg w-full"
disabled={matches?.params.id === research.id}
>
<Card
className={clsx(
Expand All @@ -56,11 +65,17 @@ export const ResearchIndexPage = () => {
</div>

<div className="flex-1">
<Suspense fallback={<CircularProgress />}>
<Suspense
fallback={
<EmptyStateCard>
<CircularProgress />
</EmptyStateCard>
}
>
<Outlet />
</Suspense>
</div>
</div>
</div>
)
}
})
Loading

0 comments on commit 4036ef3

Please sign in to comment.