diff --git a/.eslintrc.js b/.eslintrc.js index a6303b1..58059fd 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -1,5 +1,5 @@ module.exports = { - plugins: ['react', 'react-hooks', 'prettier', '@typescript-eslint'], + plugins: ['react', 'prettier', '@typescript-eslint'], extends: ['next', 'airbnb', 'prettier', 'plugin:@typescript-eslint/recommended'], parser: '@typescript-eslint/parser', globals: { @@ -21,6 +21,7 @@ module.exports = { 'react/function-component-definition': 'off', 'react/jsx-no-useless-fragment': 'off', 'react/require-default-props': 'off', + 'react/react-in-jsx-scope': 'off', 'import/extensions': 'off', 'import/no-unresolved': 'off', diff --git a/Dockerfile b/Dockerfile index f417e4b..23ea568 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20-alpine AS base +FROM node:22-alpine AS base FROM base AS builder diff --git a/next-env.d.ts b/next-env.d.ts index a4a7b3f..40c3d68 100644 --- a/next-env.d.ts +++ b/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/pages/building-your-application/configuring/typescript for more information. +// see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. diff --git a/package.json b/package.json index 6a2b63f..7139f84 100644 --- a/package.json +++ b/package.json @@ -11,37 +11,34 @@ }, "license": "MIT", "dependencies": { - "@chakra-ui/react": "^2.2.1", - "@emotion/css": "^11.11.2", - "@emotion/react": "^11.1.1", - "@emotion/styled": "^11.0.0", - "@remixicon/react": "^4.2.0", - "@supabase/auth-helpers-nextjs": "^0.9.0", - "@supabase/auth-helpers-react": "^0.4.2", - "@supabase/supabase-js": "^2.39.7", + "@chakra-ui/react": "^2.10.3", + "@dnd-kit/core": "^6.1.0", + "@dnd-kit/sortable": "^8.0.0", + "@dnd-kit/utilities": "^3.2.2", + "@emotion/css": "^11.13.4", + "@emotion/react": "^11.13.3", + "@emotion/styled": "^11.13.0", + "@remixicon/react": "^4.5.0", + "@supabase/ssr": "^0.5.1", + "@supabase/supabase-js": "^2.46.1", "framer-motion": "^6.3.12", - "next": "^14.2.10", - "next-seo": "^6.6.0", + "next": "^15.0.2", "react": "^18.2.0", - "react-beautiful-dnd": "^13.1.0", "react-dom": "^18.2.0", - "react-gesture-responder": "^2.1.0", - "react-grid-dnd": "^2.1.2", "react-hook-form": "^7.33.1", "react-hotkeys-hook": "^3.4.6", "react-markdown": "^8.0.5", "sharp": "^0.33.5" }, "devDependencies": { - "@types/node": "^20.11.24", + "@types/node": "^22.9.0", "@types/react": "~18.0.28", - "@types/react-beautiful-dnd": "^13.1.2", "@types/react-dom": "^18.2.19", "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", "eslint": "^8.57.0", "eslint-config-airbnb": "^19.0.4", - "eslint-config-next": "^14.1.0", + "eslint-config-next": "^15.0.2", "eslint-config-prettier": "^9.1.0", "eslint-plugin-import": "^2.29.1", "eslint-plugin-jsx-a11y": "^6.8.0", diff --git a/src/acitons/section-data.action.ts b/src/acitons/section-data.action.ts new file mode 100644 index 0000000..6e7e93e --- /dev/null +++ b/src/acitons/section-data.action.ts @@ -0,0 +1,23 @@ +'use server'; + +import { SchemaType } from '../types/type-util'; +import { createSupabaseClient } from '../utils/supabase/server'; + +export type SectionType = Array< + SchemaType<'sections'> & { + contents: Array & { links: SchemaType<'links'>[]; images: SchemaType<'images'>[] }>; + } +>; + +export const getSectionData = async () => { + const supabase = await createSupabaseClient(); + const { data, error } = await supabase + .from('sections') + .select('*, contents(*, links(*), images(*))') + .eq('contents.isHidden', false) + .order('id', { ascending: true }); + return { + sections: data as SectionType, + error + }; +}; diff --git a/src/pages/admin/content.tsx b/src/app/admin/content/page.tsx similarity index 88% rename from src/pages/admin/content.tsx rename to src/app/admin/content/page.tsx index 9b2e77d..06e4616 100644 --- a/src/pages/admin/content.tsx +++ b/src/app/admin/content/page.tsx @@ -1,21 +1,24 @@ /** @jsxImportSource @emotion/react */ + +'use client'; + import styled from '@emotion/styled'; import React, { useCallback, useEffect, useState } from 'react'; import { Button, Checkbox, Input, Select, Td, Textarea, useDisclosure, useToast } from '@chakra-ui/react'; import { css } from '@emotion/react'; -import { DropResult } from 'react-beautiful-dnd'; import { SubmitHandler, useForm } from 'react-hook-form'; -import useUserVerify from '../../hooks/useUserVerify'; -import { HugeTitle } from '../../components/Typography'; -import DraggableTable from '../../components/DraggableTable'; -import DeleteModal from '../../components/Dialogs/DeleteModal'; -import UpdateModal from '../../components/Dialogs/UpdateModal'; -import AdminLayout from '../../layouts/AdminLayout'; -import LinkModal from '../../components/Dialogs/LinkModal'; -import { useSupabase } from '../../utils/supabase'; -import { SchemaType } from '../../types/type-util'; -import { Space } from '../../components/Space'; -import ImageModal from '../../components/Dialogs/ImageModal'; +import { arrayMove } from '@dnd-kit/sortable'; +import { DragEndEvent } from '@dnd-kit/core'; +import { HugeTitle } from '../../../components/Typography'; +import DraggableTable from '../../../components/DraggableTable'; +import DeleteModal from '../../../components/Dialogs/DeleteModal'; +import UpdateModal from '../../../components/Dialogs/UpdateModal'; +import AdminLayout from '../../../layouts/AdminLayout'; +import LinkModal from '../../../components/Dialogs/LinkModal'; +import { SchemaType } from '../../../types/type-util'; +import { Space } from '../../../components/Space'; +import ImageModal from '../../../components/Dialogs/ImageModal'; +import { createSupabaseClient } from '../../../utils/supabase/client'; const Header = styled.div` display: grid; @@ -48,8 +51,7 @@ const DEFAULT_VALUE: AddForm = { isHidden: false }; -const AdminContent: React.FC = () => { - useUserVerify(); +const Page: React.FC = () => { const [data, setData] = useState & { sections: SchemaType<'sections'> }>>([]); const [section, setSection] = useState>>([]); const [isChange, setBeChange] = useState(false); @@ -59,13 +61,13 @@ const AdminContent: React.FC = () => { const updateDialog = useDisclosure(); const linkDialog = useDisclosure(); const imageDialog = useDisclosure(); - const supabase = useSupabase(); + const supabase = createSupabaseClient(); const toast = useToast({ isClosable: true, position: 'top-left' }); - const fetchData = async () => { + const fetchData = useCallback(async () => { const { data: response, error } = await supabase.from('contents').select('*, sections(*)'); if (error !== null) { toast({ @@ -77,14 +79,15 @@ const AdminContent: React.FC = () => { } const contents = response as Array & { sections: SchemaType<'sections'> }>; setData(contents.sort((a, b) => a.order - b.order).sort((a, b) => a.sections.order - b.sections.order)); - }; + }, [supabase]); - const onChangeData = (result: DropResult) => { - if (!result.destination) return; - const items = [...data]; - const [reorderedItem] = items.splice(result.source.index, 1); - items.splice(result.destination.index, 0, reorderedItem); - setData(items); + const onChangeData = ({ active, over }: DragEndEvent) => { + if (!over || active.id === over.id) return; + + const oldIndex = data.findIndex((item) => item.id === Number(active.id)); + const newIndex = data.findIndex((item) => item.id === Number(over.id)); + + setData((prev) => arrayMove(prev, oldIndex, newIndex)); setBeChange(true); }; @@ -123,7 +126,7 @@ const AdminContent: React.FC = () => { .then(({ data: sections }) => { if (sections !== null) setSection(sections); }); - }, [supabase]); + }, [fetchData, supabase]); const SectionOptions = useCallback( () => ( @@ -153,7 +156,7 @@ const AdminContent: React.FC = () => { {...register('title', { required: true })} /> { ); }; -export default AdminContent; +export default Page; diff --git a/src/pages/admin/login.tsx b/src/app/admin/login/page.tsx similarity index 85% rename from src/pages/admin/login.tsx rename to src/app/admin/login/page.tsx index 90a0d14..1305104 100644 --- a/src/pages/admin/login.tsx +++ b/src/app/admin/login/page.tsx @@ -1,11 +1,13 @@ +'use client'; + import React, { useState } from 'react'; import styled from '@emotion/styled'; -import { useRouter } from 'next/router'; import { useToast } from '@chakra-ui/react'; -import { SectionTitle } from '../../components/Typography'; -import Colors from '../../styles/Colors'; -import { useSupabase } from '../../utils/supabase'; -import { Space } from '../../components/Space'; +import { useRouter } from 'next/navigation'; +import { SectionTitle } from '../../../components/Typography'; +import Colors from '../../../styles/Colors'; +import { Space } from '../../../components/Space'; +import { createSupabaseClient } from '../../../utils/supabase/client'; const Container = styled.div` min-height: 100vh; @@ -45,9 +47,9 @@ const LoginButton = styled.button` } `; -const Login: React.FC = () => { +const Page: React.FC = () => { const router = useRouter(); - const supabase = useSupabase(); + const supabase = createSupabaseClient(); const [input, setInput] = useState<{ email: string; password: string }>({ email: '', password: '' @@ -108,4 +110,4 @@ const Login: React.FC = () => { ); }; -export default Login; +export default Page; diff --git a/src/pages/admin/index.tsx b/src/app/admin/page.tsx similarity index 85% rename from src/pages/admin/index.tsx rename to src/app/admin/page.tsx index 93c7306..88bf156 100644 --- a/src/pages/admin/index.tsx +++ b/src/app/admin/page.tsx @@ -1,17 +1,19 @@ -import React, { useEffect, useState } from 'react'; +'use client'; + +import React, { useCallback, useEffect, useState } from 'react'; import styled from '@emotion/styled'; import { Button, Input, useDisclosure, useToast } from '@chakra-ui/react'; -import { DropResult } from 'react-beautiful-dnd'; import { SubmitHandler, useForm } from 'react-hook-form'; -import useUserVerify from '../../hooks/useUserVerify'; +import { DragEndEvent } from '@dnd-kit/core'; +import { arrayMove } from '@dnd-kit/sortable'; import { HugeTitle } from '../../components/Typography'; import DraggableTable from '../../components/DraggableTable'; import DeleteModal from '../../components/Dialogs/DeleteModal'; import UpdateModal from '../../components/Dialogs/UpdateModal'; import AdminLayout from '../../layouts/AdminLayout'; -import { useSupabase } from '../../utils/supabase'; import { SchemaType } from '../../types/type-util'; import { Space } from '../../components/Space'; +import { createSupabaseClient } from '../../utils/supabase/client'; const Header = styled.div` display: grid; @@ -24,21 +26,20 @@ const Footer = styled.div` justify-content: flex-end; `; -const Admin: React.FC = () => { - useUserVerify(); +const Page: React.FC = () => { const [data, setData] = useState>>([]); const [isChange, setBeChange] = useState(false); const [modalData, setModalData] = useState<{ id: number; title: string }>({ id: -1, title: '' }); const { register, handleSubmit, reset } = useForm<{ title: string }>(); const deleteDialog = useDisclosure(); const updateDialog = useDisclosure(); - const supabase = useSupabase(); + const supabase = createSupabaseClient(); const toast = useToast({ isClosable: true, position: 'top-left' }); - const fetchData = async () => { + const fetchData = useCallback(async () => { const { data: sections, error } = await supabase.from('sections').select('*'); if (sections === null || error !== null) { toast({ @@ -49,11 +50,11 @@ const Admin: React.FC = () => { return; } setData(sections.sort((a, b) => a.order - b.order)); - }; + }, [supabase]); useEffect(() => { fetchData().then(); - }, []); + }, [fetchData]); const onAddClick: SubmitHandler<{ title: string }> = async (values) => { if (values.title.trim() === '') return; @@ -62,12 +63,13 @@ const Admin: React.FC = () => { reset({ title: '' }); }; - const onChangeData = (result: DropResult) => { - if (!result.destination) return; - const items = [...data]; - const [reorderedItem] = items.splice(result.source.index, 1); - items.splice(result.destination.index, 0, reorderedItem); - setData(items); + const onChangeData = ({ active, over }: DragEndEvent) => { + if (!over || active.id === over.id) return; + + const oldIndex = data.findIndex((item) => item.id === Number(active.id)); + const newIndex = data.findIndex((item) => item.id === Number(over.id)); + + setData((prev) => arrayMove(prev, oldIndex, newIndex)); setBeChange(true); }; @@ -94,10 +96,10 @@ const Admin: React.FC = () => { { + onKeyDown={(e) => { if (e.key === 'Enter') handleSubmit(onAddClick)(); }} + {...register('title', { required: true })} />