✨ Other user profile page
This commit is contained in:
		| @@ -1,8 +1,11 @@ | |||||||
| import type { NextConfig } from "next"; | import type { NextConfig } from 'next' | ||||||
|  |  | ||||||
| const nextConfig: NextConfig = { | const nextConfig: NextConfig = { | ||||||
|   /* config options here */ |   /* config options here */ | ||||||
|   reactStrictMode: true, |   reactStrictMode: true, | ||||||
| }; |   images: { | ||||||
|  |     domains: ['raw.sn.solsynth.dev', 'api.sn.solsynth.dev'], | ||||||
|  |   }, | ||||||
|  | } | ||||||
|  |  | ||||||
| export default nextConfig; | export default nextConfig | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ | |||||||
|     "@emotion/styled": "^11.14.0", |     "@emotion/styled": "^11.14.0", | ||||||
|     "@mui/icons-material": "^6.3.0", |     "@mui/icons-material": "^6.3.0", | ||||||
|     "@mui/material": "^6.3.0", |     "@mui/material": "^6.3.0", | ||||||
|  |     "@mui/x-charts": "^7.23.2", | ||||||
|     "axios": "^1.7.9", |     "axios": "^1.7.9", | ||||||
|     "axios-case-converter": "^1.1.1", |     "axios-case-converter": "^1.1.1", | ||||||
|     "cookies-next": "^5.0.2", |     "cookies-next": "^5.0.2", | ||||||
|   | |||||||
							
								
								
									
										114
									
								
								src/pages/users/[name].tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								src/pages/users/[name].tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,114 @@ | |||||||
|  | import { SnCheckInRecord } from '@/services/checkIn' | ||||||
|  | import { getAttachmentUrl, sni } from '@/services/network' | ||||||
|  | import { SnAccount } from '@/services/user' | ||||||
|  | import { Avatar, Box, Card, CardContent, Container, Grid2 as Grid, Typography } from '@mui/material' | ||||||
|  | import { LineChart } from '@mui/x-charts' | ||||||
|  | import type { InferGetServerSidePropsType, GetServerSideProps } from 'next' | ||||||
|  | import Image from 'next/image' | ||||||
|  |  | ||||||
|  | export const getServerSideProps = (async (context) => { | ||||||
|  |   const name = context.params!.name as string | ||||||
|  |   try { | ||||||
|  |     const { data: user } = await sni.get<SnAccount>('/cgi/id/users/' + name) | ||||||
|  |     const { data: checkIn } = await sni.get<{ data: SnCheckInRecord[] }>('/cgi/id/users/' + name + '/check-in', { | ||||||
|  |       params: { take: 14 }, | ||||||
|  |     }) | ||||||
|  |     return { props: { user, checkIn: checkIn.data } } | ||||||
|  |   } catch (err) { | ||||||
|  |     return { | ||||||
|  |       notFound: true, | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | }) satisfies GetServerSideProps<{ user: SnAccount; checkIn: SnCheckInRecord[] }> | ||||||
|  |  | ||||||
|  | export default function UserProfile({ user, checkIn }: InferGetServerSidePropsType<typeof getServerSideProps>) { | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       {user.banner && ( | ||||||
|  |         <Box sx={{ aspectRatio: 16 / 5, position: 'relative' }}> | ||||||
|  |           <Image src={getAttachmentUrl(user.banner)} alt="account banner" style={{ objectFit: 'cover' }} fill /> | ||||||
|  |         </Box> | ||||||
|  |       )} | ||||||
|  |  | ||||||
|  |       <Container sx={{ mt: 4, px: 2 }}> | ||||||
|  |         <Box sx={{ display: 'flex', justifyContent: 'space-between' }}> | ||||||
|  |           <Box sx={{ display: 'flex', gap: 2 }}> | ||||||
|  |             {user && <Avatar src={getAttachmentUrl(user.avatar)} />} | ||||||
|  |             <Box sx={{ display: 'flex', flexDirection: 'column' }}> | ||||||
|  |               <Typography fontWeight="bold">{user.nick}</Typography> | ||||||
|  |               <Typography fontFamily="monospace" fontSize={13} lineHeight={1.2}> | ||||||
|  |                 @{user.name} | ||||||
|  |               </Typography> | ||||||
|  |             </Box> | ||||||
|  |           </Box> | ||||||
|  |         </Box> | ||||||
|  |  | ||||||
|  |         <Grid container spacing={2} sx={{ mt: 3 }}> | ||||||
|  |           <Grid size={8}> | ||||||
|  |             <Card> | ||||||
|  |               <CardContent> | ||||||
|  |                 <Typography variant="h6" gutterBottom> | ||||||
|  |                   Fortune History | ||||||
|  |                 </Typography> | ||||||
|  |                 <LineChart | ||||||
|  |                   yAxis={[ | ||||||
|  |                     { | ||||||
|  |                       data: [1, 2, 3, 4, 5], | ||||||
|  |                       tickMinStep: 1, | ||||||
|  |                       tickMaxStep: 1, | ||||||
|  |                       valueFormatter(value, _) { | ||||||
|  |                         const resultTierList = ['大凶', '凶', '中平', '吉', '大吉'] | ||||||
|  |                         return resultTierList[value] | ||||||
|  |                       }, | ||||||
|  |                     }, | ||||||
|  |                   ]} | ||||||
|  |                   xAxis={[ | ||||||
|  |                     { | ||||||
|  |                       scaleType: 'time', | ||||||
|  |                       data: checkIn.map((c) => { | ||||||
|  |                         const og = new Date(c.createdAt) | ||||||
|  |                         og.setHours(0, 0, 0, 0) | ||||||
|  |                         return og | ||||||
|  |                       }), | ||||||
|  |                       valueFormatter(value, _) { | ||||||
|  |                         return new Date(value).toLocaleDateString('en-US', { | ||||||
|  |                           month: '2-digit', | ||||||
|  |                           day: '2-digit', | ||||||
|  |                         }) | ||||||
|  |                       }, | ||||||
|  |                     }, | ||||||
|  |                   ]} | ||||||
|  |                   series={[ | ||||||
|  |                     { | ||||||
|  |                       data: checkIn.map((c) => c.resultTier), | ||||||
|  |                       valueFormatter(value, _) { | ||||||
|  |                         const resultTierList = ['大凶', '凶', '中平', '吉', '大吉'] | ||||||
|  |                         return resultTierList[value ?? 0] | ||||||
|  |                       }, | ||||||
|  |                     }, | ||||||
|  |                   ]} | ||||||
|  |                   height={300} | ||||||
|  |                   margin={{ top: 16, bottom: 24 }} | ||||||
|  |                 /> | ||||||
|  |               </CardContent> | ||||||
|  |             </Card> | ||||||
|  |           </Grid> | ||||||
|  |           <Grid size={4}> | ||||||
|  |             <Card> | ||||||
|  |               <CardContent> | ||||||
|  |                 <Typography variant="h6" gutterBottom> | ||||||
|  |                   Information | ||||||
|  |                 </Typography> | ||||||
|  |  | ||||||
|  |                 <Typography variant="body2"> | ||||||
|  |                   Born on {new Date(user.profile!.birthday!).toLocaleDateString()} | ||||||
|  |                 </Typography> | ||||||
|  |                 <Typography variant="body2">Joined at {new Date(user.createdAt).toLocaleDateString()}</Typography> | ||||||
|  |               </CardContent> | ||||||
|  |             </Card> | ||||||
|  |           </Grid> | ||||||
|  |         </Grid> | ||||||
|  |       </Container> | ||||||
|  |     </> | ||||||
|  |   ) | ||||||
|  | } | ||||||
| @@ -1,12 +1,40 @@ | |||||||
| import { checkAuthenticatedClient, redirectToLogin } from '@/services/auth' | import { checkAuthenticatedClient, redirectToLogin } from '@/services/auth' | ||||||
| import { Container } from '@mui/material' | import { useUserStore } from '@/services/user' | ||||||
| import { useRouter } from 'next/router' | import { Avatar, Box, Container, Typography } from '@mui/material' | ||||||
|  | import { getAttachmentUrl } from '@/services/network' | ||||||
| import { useEffect } from 'react' | import { useEffect } from 'react' | ||||||
|  | import Image from 'next/image' | ||||||
|  |  | ||||||
| export default function UserItself() { | export default function UserItself() { | ||||||
|   useEffect(() => { |   useEffect(() => { | ||||||
|     if (!checkAuthenticatedClient()) redirectToLogin() |     if (!checkAuthenticatedClient()) redirectToLogin() | ||||||
|   }, []) |   }, []) | ||||||
|  |  | ||||||
|   return <Container></Container> |   const userStore = useUserStore() | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       {userStore.account && ( | ||||||
|  |         <Box sx={{ aspectRatio: 16 / 5, position: 'relative' }}> | ||||||
|  |           <Image src={getAttachmentUrl(userStore.account!.banner)} alt="account banner" objectFit="cover" fill /> | ||||||
|  |         </Box> | ||||||
|  |       )} | ||||||
|  |  | ||||||
|  |       <Container sx={{ mt: 5 }}> | ||||||
|  |         <Box sx={{ display: 'flex', justifyContent: 'space-between' }}> | ||||||
|  |           <Box sx={{ display: 'flex', gap: 2 }}> | ||||||
|  |             {userStore.account && <Avatar src={getAttachmentUrl(userStore.account!.avatar)} />} | ||||||
|  |             <Box sx={{ display: 'flex', flexDirection: 'column' }}> | ||||||
|  |               <Typography fontWeight="bold">{userStore.account?.nick}</Typography> | ||||||
|  |               <Typography fontFamily="monospace" fontSize={13}> | ||||||
|  |                 @{userStore.account?.name} | ||||||
|  |               </Typography> | ||||||
|  |             </Box> | ||||||
|  |           </Box> | ||||||
|  |         </Box> | ||||||
|  |  | ||||||
|  |         <Box sx={{ mt: 5 }}></Box> | ||||||
|  |       </Container> | ||||||
|  |     </> | ||||||
|  |   ) | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										10
									
								
								src/services/checkIn.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/services/checkIn.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,10 @@ | |||||||
|  | export interface SnCheckInRecord { | ||||||
|  |   id: number | ||||||
|  |   createdAt: Date | ||||||
|  |   updatedAt: Date | ||||||
|  |   deletedAt?: Date | null | ||||||
|  |   resultTier: number | ||||||
|  |   resultExperience: number | ||||||
|  |   resultModifiers: number[] | ||||||
|  |   accountId: number | ||||||
|  | } | ||||||
| @@ -2,7 +2,7 @@ import { create } from 'zustand' | |||||||
| import { sni } from './network' | import { sni } from './network' | ||||||
| import { hasCookie } from 'cookies-next/client' | import { hasCookie } from 'cookies-next/client' | ||||||
|  |  | ||||||
| interface SnAccount { | export interface SnAccount { | ||||||
|   id: number |   id: number | ||||||
|   createdAt: Date |   createdAt: Date | ||||||
|   updatedAt: Date |   updatedAt: Date | ||||||
| @@ -24,7 +24,7 @@ interface SnAccount { | |||||||
|   automatedId?: number | null |   automatedId?: number | null | ||||||
| } | } | ||||||
|  |  | ||||||
| interface SnAccountContact { | export interface SnAccountContact { | ||||||
|   accountId: number |   accountId: number | ||||||
|   content: string |   content: string | ||||||
|   createdAt: Date |   createdAt: Date | ||||||
| @@ -37,7 +37,7 @@ interface SnAccountContact { | |||||||
|   verifiedAt?: Date | null |   verifiedAt?: Date | null | ||||||
| } | } | ||||||
|  |  | ||||||
| interface SnAccountProfile { | export interface SnAccountProfile { | ||||||
|   id: number |   id: number | ||||||
|   accountId: number |   accountId: number | ||||||
|   birthday?: Date | null |   birthday?: Date | null | ||||||
| @@ -50,7 +50,7 @@ interface SnAccountProfile { | |||||||
|   updatedAt: Date |   updatedAt: Date | ||||||
| } | } | ||||||
|  |  | ||||||
| interface SnAccountBadge { | export interface SnAccountBadge { | ||||||
|   id: number |   id: number | ||||||
|   createdAt: Date |   createdAt: Date | ||||||
|   updatedAt: Date |   updatedAt: Date | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user