Skip to content

Commit

Permalink
feat: add settings page (#7)
Browse files Browse the repository at this point in the history
  • Loading branch information
exKAZUu authored Jan 11, 2024
1 parent 6130783 commit 4b674f7
Show file tree
Hide file tree
Showing 14 changed files with 167 additions and 92 deletions.
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@emotion/react": "11.11.3",
"@emotion/styled": "11.11.0",
"@prisma/client": "5.7.1",
"@tanstack/react-query": "5.17.9",
"@willbooster/shared-lib-react": "3.0.5",
"framer-motion": "10.17.9",
"next": "14.0.4",
Expand All @@ -47,7 +48,8 @@
"supertokens-auth-react": "0.36.1",
"supertokens-node": "16.6.8",
"supertokens-web-js": "0.8.0",
"zod": "3.22.4"
"zod": "3.22.4",
"zod-form-data": "2.0.2"
},
"devDependencies": {
"@chakra-ui/cli": "2.4.1",
Expand Down
10 changes: 0 additions & 10 deletions prisma/migrations/20231228091812_init/migration.sql

This file was deleted.

10 changes: 10 additions & 0 deletions prisma/migrations/20240111063057_init/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
-- CreateTable
CREATE TABLE "User" (
"id" TEXT NOT NULL PRIMARY KEY,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL,
"displayName" TEXT NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "User_displayName_key" ON "User"("displayName");
4 changes: 2 additions & 2 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,9 @@ datasource db {
// -----------------------------------------------------------------------------

model User {
id Int @id @default(autoincrement())
id String @id
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
superTokensUserId String @unique
displayName String @unique
}
6 changes: 3 additions & 3 deletions src/app/(withAuth)/courses/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import type { NextPage } from 'next';

import { getSessionOnServerPage } from '../../../infrastructures/session';
import { getNonNullableSessionOnServer } from '../../../utils/session';

const CoursePage: NextPage = async () => {
const session = await getSessionOnServerPage();
const session = await getNonNullableSessionOnServer();

return (
<main>
<div>UserID on Supertokens: {session.getUserId()}</div>
<div>UserID: {session.getUserId()}</div>
<h1>Courses</h1>
</main>
);
Expand Down
6 changes: 2 additions & 4 deletions src/app/(withAuth)/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import { SessionAuthForNext } from '../../components/molecules/SessionAuthForNex
import { TryRefreshComponent } from '../../components/molecules/TryRefreshComponent';
import { DefaultFooter } from '../../components/organisms/DefaultFooter';
import { DefaultHeader } from '../../components/organisms/DefaultHeader';
import { getSessionOnServerLayout } from '../../infrastructures/session';
import type { LayoutProps } from '../../types';
import { getNullableSessionOnServer } from '../../utils/session';

const DefaultLayout: NextPage<LayoutProps> = async ({ children }) => {
const { hasInvalidClaims, hasToken, session } = await getSessionOnServerLayout();
const { hasInvalidClaims, hasToken, session } = await getNullableSessionOnServer();

// `session` will be undefined if it does not exist or has expired
if (!session) {
Expand Down Expand Up @@ -54,13 +54,11 @@ const DefaultLayout: NextPage<LayoutProps> = async ({ children }) => {
<SessionAuthForNext />

<DefaultHeader />

<Suspense fallback={<Spinner left="50%" position="fixed" top="50%" transform="translate(-50%, -50%)" />}>
<Container pb={16} pt={8}>
{children}
</Container>
</Suspense>

<DefaultFooter />
</>
);
Expand Down
54 changes: 54 additions & 0 deletions src/app/(withAuth)/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Box, Button, FormControl, FormLabel, Input } from '@chakra-ui/react';
import type { NextPage } from 'next';
import { revalidatePath } from 'next/cache';
import { z } from 'zod';
import { zfd } from 'zod-form-data';

import { prisma } from '../../../infrastructures/prisma';
import { getNonNullableSessionOnServer } from '../../../utils/session';

const SettingsPage: NextPage = async () => {
const session = await getNonNullableSessionOnServer();
const user = await prisma.user.findUnique({
where: {
id: session.getUserId(),
},
});

return (
<Box>
<form action={updateDisplayName}>
<FormControl>
<FormLabel>あなたの表示名</FormLabel>
<Input defaultValue={user?.displayName} name="displayName" type="text" />
</FormControl>
<Button colorScheme="blue" mt={4} type="submit">
更新
</Button>
</form>
</Box>
);
};

const inputSchema = zfd.formData({
displayName: z.string().min(1),
});

async function updateDisplayName(formData: FormData): Promise<void> {
'use server';
const input = inputSchema.parse(formData);
const session = await getNonNullableSessionOnServer();
await prisma.user.update({
where: {
id: session.getUserId(),
},
data: {
displayName: input.displayName,
},
});

// ユーザ名の変更を全ページに反映する。
revalidatePath('/', 'layout');
}

export default SettingsPage;
4 changes: 0 additions & 4 deletions src/app/(withoutAuth)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
'use client';

import { Container, Spinner } from '@chakra-ui/react';
import React, { Suspense } from 'react';

Expand All @@ -11,13 +9,11 @@ const DefaultLayout: LayoutComponent = ({ children }) => {
return (
<>
<DefaultHeader />

<Suspense fallback={<Spinner left="50%" position="fixed" top="50%" transform="translate(-50%, -50%)" />}>
<Container pb={16} pt={8}>
{children}
</Container>
</Suspense>

<DefaultFooter />
</>
);
Expand Down
13 changes: 1 addition & 12 deletions src/app/(withoutAuth)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { Button, Heading, Text, VStack } from '@chakra-ui/react';
import { Button, Heading, VStack } from '@chakra-ui/react';
import type { NextPage } from 'next';
import NextLink from 'next/link';

import { prisma } from '../../infrastructures/prisma';

const HomePage: NextPage = async () => {
const users = await prisma.user.findMany();

return (
<VStack align="stretch" spacing={16}>
<VStack bg="gray.100" py={16} rounded="xl" spacing={8}>
Expand All @@ -16,13 +12,6 @@ const HomePage: NextPage = async () => {
<Button as={NextLink} colorScheme="brand" href="/courses" size="lg">
今すぐはじめる
</Button>
<ul>
{users.map((user) => (
<li key={user.id}>
<Text>{user.superTokensUserId}</Text>
</li>
))}
</ul>
</VStack>
</VStack>
);
Expand Down
65 changes: 23 additions & 42 deletions src/components/organisms/DefaultHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,38 +1,32 @@
'use client';

import type { BoxProps } from '@chakra-ui/react';
import {
Box,
Button,
Heading,
HStack,
Icon,
Menu,
MenuButton,
MenuDivider,
MenuItem,
MenuList,
} from '@chakra-ui/react';
import { Box, Button, Heading, HStack, Menu, MenuButton, MenuDivider, MenuItem, MenuList } from '@chakra-ui/react';
import type { NextPage } from 'next';
import NextLink from 'next/link';
import { useRouter } from 'next/navigation';
import React from 'react';
import { FaChevronDown, FaCog, FaSignOutAlt, FaUser } from 'react-icons/fa';
import { signOut, useSessionContext } from 'supertokens-auth-react/recipe/session';
import { FaChevronDown, FaCog, FaUser } from 'react-icons/fa';

import { APP_NAME } from '../../constants';
import { prisma } from '../../infrastructures/prisma';
import { getNullableSessionOnServer } from '../../utils/session';

import { SignOutMenuItem } from './SignOutMenuItem';

const MENU_ITEMS: readonly [string, string][] = [
['/courses', '科目一覧'],
['/submissions', '提出一覧'],
];

export const DefaultHeader: React.FC<BoxProps> = (props) => {
const session = useSessionContext();

const router = useRouter();
export const DefaultHeader: NextPage = async () => {
const { session } = await getNullableSessionOnServer();
const user =
session &&
(await prisma.user.findUnique({
where: {
id: session.getUserId(),
},
}));

return (
<HStack borderBottomWidth={1} h={16} px={4} spacing={4} {...props}>
<HStack borderBottomWidth={1} h={16} px={4} spacing={4}>
<HStack flexGrow={1} flexShrink={1} spacing={8}>
<Heading as={NextLink} href="/" size="md">
{APP_NAME}
Expand All @@ -46,34 +40,21 @@ export const DefaultHeader: React.FC<BoxProps> = (props) => {
</HStack>
</HStack>
<Box flexGrow={0} flexShrink={0}>
{!session.loading && session.doesSessionExist ? (
{user?.displayName ? (
<Menu direction="rtl">
<MenuButton
as={Button}
leftIcon={<Icon as={FaUser} boxSize={5} />}
rightIcon={<FaChevronDown />}
variant="ghost"
>
{`User ${session.userId.slice(0, 4)}...${session.userId.slice(-4)}`}
<MenuButton as={Button} leftIcon={<FaUser size="1em" />} rightIcon={<FaChevronDown />} variant="ghost">
{user.displayName}
</MenuButton>
<MenuList>
<MenuItem as={NextLink} href="/settings" icon={<Icon as={FaCog} boxSize={5} />}>
<MenuItem as={NextLink} href="/settings" icon={<FaCog size="1.5em" />}>
設定
</MenuItem>
<MenuDivider />
<MenuItem
icon={<Icon as={FaSignOutAlt} boxSize={5} />}
onClick={async () => {
await signOut();
router.push('/');
}}
>
サインアウト
</MenuItem>
<SignOutMenuItem />
</MenuList>
</Menu>
) : (
<Button as={NextLink} colorScheme="brand" href="/auth" isLoading={session.loading}>
<Button as={NextLink} colorScheme="brand" href="/auth">
サインイン
</Button>
)}
Expand Down
13 changes: 9 additions & 4 deletions src/components/organisms/Providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { CacheProvider } from '@chakra-ui/next-js';
import { ChakraProvider } from '@chakra-ui/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { usePathname, useRouter } from 'next/navigation';
import React from 'react';
import { SuperTokensWrapper } from 'supertokens-auth-react';
Expand All @@ -11,15 +12,19 @@ import { theme } from '../../theme';

ensureSuperTokensReactInit();

const queryClient = new QueryClient();

export const Providers: React.FC<{ children: React.ReactNode }> = ({ children }) => {
setRouter(useRouter(), usePathname() || window.location.pathname);

return (
<SuperTokensWrapper>
{/* Chakra UI */}
<CacheProvider>
<ChakraProvider theme={theme}>{children}</ChakraProvider>
</CacheProvider>
<QueryClientProvider client={queryClient}>
{/* Chakra UI */}
<CacheProvider>
<ChakraProvider theme={theme}>{children}</ChakraProvider>
</CacheProvider>
</QueryClientProvider>
</SuperTokensWrapper>
);
};
21 changes: 21 additions & 0 deletions src/components/organisms/SignOutMenuItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client';
import { MenuItem } from '@chakra-ui/react';
import { useRouter } from 'next/navigation';
import React from 'react';
import { FaSignOutAlt } from 'react-icons/fa';
import { signOut } from 'supertokens-auth-react/recipe/session';

export const SignOutMenuItem: React.FC = () => {
const router = useRouter();
return (
<MenuItem
icon={<FaSignOutAlt size="1.5em" />}
onClick={async () => {
await signOut();
router.push('/');
}}
>
サインアウト
</MenuItem>
);
};
Loading

0 comments on commit 4b674f7

Please sign in to comment.