20 changed files with 349 additions and 57 deletions
@ -0,0 +1,2 @@ |
|||||
|
# appwrite的项目ID |
||||
|
VUE_APPWRITE_PROJECT_ID= |
@ -0,0 +1,16 @@ |
|||||
|
import { Toast } from "@/utils/toast" |
||||
|
import { Account, Client, ID } from "appwrite" |
||||
|
|
||||
|
const client = new Client() |
||||
|
|
||||
|
client.setProject(import.meta.env.VUE_APPWRITE_PROJECT_ID) |
||||
|
|
||||
|
export async function register(opts: { email: string; password: string }, errText: string) { |
||||
|
const account = new Account(client) |
||||
|
try { |
||||
|
await account.create(ID.unique(), opts.email, opts.password) |
||||
|
} catch (error: any) { |
||||
|
Toast.error(error?.message ?? errText) |
||||
|
return Promise.reject(error) |
||||
|
} |
||||
|
} |
@ -0,0 +1,9 @@ |
|||||
|
import { atom } from "jotai" |
||||
|
|
||||
|
const userStore = { |
||||
|
userAtom: atom({ |
||||
|
token: "", |
||||
|
}), |
||||
|
} |
||||
|
|
||||
|
export { userStore } |
@ -0,0 +1,52 @@ |
|||||
|
import { OpacityIn } from "@/effect" |
||||
|
import styled from "styled-components" |
||||
|
|
||||
|
const FigureElement = styled.figure` |
||||
|
margin: 0; |
||||
|
position: relative; |
||||
|
height: 350px; |
||||
|
.img-wrapper { |
||||
|
height: 100%; |
||||
|
width: 100%; |
||||
|
filter: brightness(0.75); |
||||
|
img { |
||||
|
height: 100%; |
||||
|
width: 100%; |
||||
|
object-fit: cover; |
||||
|
object-position: top -100px right 0; |
||||
|
display: block; |
||||
|
} |
||||
|
} |
||||
|
.mask { |
||||
|
background: url(data:image/webp;base64,UklGRkYAAABXRUJQVlA4WAoAAAAQAAAAAQAAAQAAQUxQSAUAAAAA/wAAAABWUDggGgAAADABAJ0BKgIAAgAAwBIlpAADcAD+/u6qAAAA); |
||||
|
position: absolute; |
||||
|
width: 100%; |
||||
|
height: 100%; |
||||
|
top: 0; |
||||
|
} |
||||
|
figcaption { |
||||
|
position: absolute; |
||||
|
left: 50%; |
||||
|
top: 50%; |
||||
|
transform: translate(-50%, -50%); |
||||
|
color: white; |
||||
|
padding: 15px 20px; |
||||
|
font-size: 2em; |
||||
|
text-align: justify; |
||||
|
} |
||||
|
` |
||||
|
|
||||
|
export function Hero() { |
||||
|
return ( |
||||
|
<FigureElement> |
||||
|
<OpacityIn className="img-wrapper"> |
||||
|
<img |
||||
|
src="https://api.r10086.com/%E5%9B%BE%E5%8C%85webp/%E5%8A%A8%E6%BC%AB%E7%BB%BC%E5%90%882/48cf0b92dd80ccdd4898e6b4734105fc.png!q90.webp" |
||||
|
alt="月宫" |
||||
|
/> |
||||
|
</OpacityIn> |
||||
|
<div className="mask"></div> |
||||
|
<figcaption>珠光照月,斑驳裂影。桃艳压山,风月无边。</figcaption> |
||||
|
</FigureElement> |
||||
|
) |
||||
|
} |
@ -0,0 +1,22 @@ |
|||||
|
import ReactMarkdown from "react-markdown" |
||||
|
import styled from "styled-components" |
||||
|
|
||||
|
const MarkdownElement = styled(ReactMarkdown)` |
||||
|
blockquote { |
||||
|
margin: 0; |
||||
|
background-color: gainsboro; |
||||
|
padding: 10px 10px 10px 10px; |
||||
|
p:last-child { |
||||
|
margin-bottom: 0; |
||||
|
font-size: 1.1em; |
||||
|
} |
||||
|
} |
||||
|
` |
||||
|
|
||||
|
interface IProps { |
||||
|
children: string |
||||
|
} |
||||
|
|
||||
|
export function Markdown({ children }: IProps) { |
||||
|
return <MarkdownElement>{children}</MarkdownElement> |
||||
|
} |
@ -0,0 +1,11 @@ |
|||||
|
// import { Tag } from "@blueprintjs/core";
|
||||
|
|
||||
|
export function Notice() { |
||||
|
return ( |
||||
|
<> |
||||
|
{/* <Tag large round icon="notifications" intent="warning" interactive> |
||||
|
公告: 吾生也有涯,而知也无涯。以有涯随无涯,殆已! |
||||
|
</Tag> */} |
||||
|
</> |
||||
|
) |
||||
|
} |
@ -0,0 +1,111 @@ |
|||||
|
// import { Tag } from "@blueprintjs/core";
|
||||
|
|
||||
|
import { register } from "@/api" |
||||
|
import { Toast } from "@/utils/toast" |
||||
|
import { Button, FormGroup, InputGroup, Intent, Tooltip } from "@blueprintjs/core" |
||||
|
import { FC, ReactNode, useCallback, useEffect, useState } from "react" |
||||
|
|
||||
|
interface IProps { |
||||
|
onSuccess?: () => void |
||||
|
children?: { |
||||
|
Wrapper?: FC<{ children: ReactNode }> |
||||
|
Default?: FC<{ children: ReactNode }> //({ children }: { children: ReactNode }) => JSX.Element
|
||||
|
} |
||||
|
} |
||||
|
|
||||
|
export function Register({ onSuccess, children }: IProps) { |
||||
|
const [isLoading, setIsLoading] = useState(false) |
||||
|
const [showPassword, setShowPassword] = useState(false) |
||||
|
const [email, setEmail] = useState("") |
||||
|
const [password, setPassword] = useState("") |
||||
|
const [repeatPassword, setRepeatPassword] = useState("") |
||||
|
|
||||
|
const clickRegister = useCallback(async () => { |
||||
|
if (isLoading) { |
||||
|
Toast.success("正在请求中") |
||||
|
return |
||||
|
} |
||||
|
if (!email) return Toast.error("请输入邮箱") |
||||
|
if (!password) return Toast.error("请输入密码") |
||||
|
if (!repeatPassword) return Toast.error("请再一次输入密码") |
||||
|
if (repeatPassword !== password) return Toast.error("两次输入的密码不一致") |
||||
|
let toastID = Toast.loading("注册中") |
||||
|
setIsLoading(true) |
||||
|
try { |
||||
|
await register({ email, password }, "注册失败") |
||||
|
Toast.success("注册成功") |
||||
|
onSuccess && onSuccess() |
||||
|
} catch (error) { |
||||
|
console.error(error) |
||||
|
} finally { |
||||
|
Toast.loadingDismiss(toastID) |
||||
|
setIsLoading(false) |
||||
|
} |
||||
|
}, [password, email, repeatPassword, isLoading]) |
||||
|
|
||||
|
useEffect(()=>{ |
||||
|
return ()=>{ |
||||
|
console.log("取消请求"); |
||||
|
} |
||||
|
}, []) |
||||
|
|
||||
|
const WrapperComp = useCallback( |
||||
|
({ children: child }: { children: ReactNode }) => children?.Wrapper?.({ children: child }) ?? <>{child}</>, |
||||
|
[children?.Wrapper], |
||||
|
) |
||||
|
const DefaultComp = useCallback( |
||||
|
({ children: child }: { children: ReactNode }) => children?.Default?.({ children: child }) ?? <>{child}</>, |
||||
|
[children?.Default], |
||||
|
) |
||||
|
|
||||
|
return ( |
||||
|
<> |
||||
|
<form onSubmitCapture={clickRegister}> |
||||
|
<WrapperComp> |
||||
|
<FormGroup helperText="请输入一个可用的邮箱" label="邮箱" labelFor="text-input" labelInfo="(必须)"> |
||||
|
<InputGroup |
||||
|
value={email} |
||||
|
onChange={v => setEmail(v.target.value)} |
||||
|
id="email-input" |
||||
|
placeholder="请输入邮箱" |
||||
|
type="text" |
||||
|
/> |
||||
|
</FormGroup> |
||||
|
<FormGroup helperText="请输入密码" label="密码" labelFor="pwd-input" labelInfo="(必须)"> |
||||
|
<InputGroup |
||||
|
id="pwd-input" |
||||
|
value={password} |
||||
|
onChange={v => setPassword(v.target.value)} |
||||
|
placeholder="请输入密码" |
||||
|
rightElement={ |
||||
|
<Tooltip content={`${showPassword ? "Hide" : "Show"} Password`}> |
||||
|
<Button |
||||
|
icon={showPassword ? "unlock" : "lock"} |
||||
|
intent={Intent.WARNING} |
||||
|
minimal={true} |
||||
|
onClick={() => setShowPassword(!showPassword)} |
||||
|
/> |
||||
|
</Tooltip> |
||||
|
} |
||||
|
type={showPassword ? "text" : "password"} |
||||
|
/> |
||||
|
</FormGroup> |
||||
|
<FormGroup helperText="请再输入一遍密码" label="重复密码" labelFor="rpwd-input" labelInfo="(必须)"> |
||||
|
<InputGroup |
||||
|
value={repeatPassword} |
||||
|
onChange={v => setRepeatPassword(v.target.value)} |
||||
|
id="rpwd-input" |
||||
|
placeholder="请输入密码" |
||||
|
type="text" |
||||
|
/> |
||||
|
</FormGroup> |
||||
|
</WrapperComp> |
||||
|
<DefaultComp> |
||||
|
<Button intent="primary" type="submit"> |
||||
|
注册 |
||||
|
</Button> |
||||
|
</DefaultComp> |
||||
|
</form> |
||||
|
</> |
||||
|
) |
||||
|
} |
@ -0,0 +1,21 @@ |
|||||
|
import { OverlayToaster, Position, Toaster } from "@blueprintjs/core" |
||||
|
|
||||
|
interface Toaster2 { |
||||
|
error: Function |
||||
|
success: Function |
||||
|
loading: Function |
||||
|
loadingDismiss: Function |
||||
|
} |
||||
|
|
||||
|
export const Toast: Toaster & Toaster2 = OverlayToaster.create({ position: Position.TOP_RIGHT }) as any |
||||
|
export const TToast: Toaster & Toaster2 = OverlayToaster.create({ position: Position.TOP }) as any |
||||
|
|
||||
|
Toast.loadingDismiss = (key: string) => { |
||||
|
TToast.dismiss(key) |
||||
|
} |
||||
|
Toast.error = (message: string) => Toast.show({ message: message, icon: "error", intent: "danger", timeout: 3000 }) |
||||
|
Toast.success = (message: string) => Toast.show({ message: message, intent: "success", timeout: 3000 }) |
||||
|
Toast.loading = (message: string) => { |
||||
|
const key = TToast.show({ message: message, intent: "primary", timeout: 0 }) |
||||
|
return key |
||||
|
} |
@ -1,37 +1,19 @@ |
|||||
import withPage from "@/base/withPage" |
import withPage from "@/base/withPage" |
||||
import { useHistory } from "react-router-dom" |
import { Hero } from "@/ui/Hero" |
||||
import styled from "styled-components" |
import { ReactNode } from "react" |
||||
import ReactMarkdown from "react-markdown" |
|
||||
import MdText from "./index.md?raw" |
|
||||
|
|
||||
const Title = styled.h1` |
interface IProps { |
||||
font-size: 1.5em; |
children: ReactNode |
||||
text-align: center; |
} |
||||
color: #bf4f74; |
|
||||
` |
|
||||
|
|
||||
const Wrapper = styled.section` |
export default withPage(function Project({}: IProps) { |
||||
padding: 4em; |
|
||||
background: papayawhip; |
|
||||
` |
|
||||
|
|
||||
export default withPage(function Project({ children }) { |
|
||||
const router = useHistory() |
|
||||
function toChild() { |
|
||||
router.push("/project/child") |
|
||||
} |
|
||||
return ( |
return ( |
||||
<div className="container"> |
<> |
||||
<Wrapper> |
<Hero></Hero> |
||||
<Title>HOME</Title> |
|
||||
</Wrapper> |
<div className="container"> |
||||
<ReactMarkdown>{MdText}</ReactMarkdown> |
|
||||
<div> |
|
||||
<div onClick={toChild} style={{ height: "200px" }}> |
|
||||
vaas |
|
||||
</div> |
|
||||
{children} |
|
||||
</div> |
</div> |
||||
</div> |
</> |
||||
) |
) |
||||
}) |
}) |
||||
|
Loading…
Reference in new issue