diff --git a/api/db/migrations/20241005014130_/migration.sql b/api/db/migrations/20241005014130_/migration.sql new file mode 100644 index 0000000..1ac09bb --- /dev/null +++ b/api/db/migrations/20241005014130_/migration.sql @@ -0,0 +1,16 @@ +/* + Warnings: + + - You are about to drop the `Title` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropTable +DROP TABLE "Title"; + +-- CreateTable +CREATE TABLE "Titles" ( + "id" SERIAL NOT NULL, + "titles" TEXT[] DEFAULT ARRAY[]::TEXT[], + + CONSTRAINT "Titles_pkey" PRIMARY KEY ("id") +); diff --git a/api/db/schema.prisma b/api/db/schema.prisma index 1cd8215..f7bab9a 100644 --- a/api/db/schema.prisma +++ b/api/db/schema.prisma @@ -63,9 +63,9 @@ model Resume { fileId String } -model Title { - id Int @id @default(autoincrement()) - title String +model Titles { + id Int @id @default(autoincrement()) + titles String[] @default([]) } model Tag { diff --git a/api/src/graphql/title.sdl.ts b/api/src/graphql/title.sdl.ts new file mode 100644 index 0000000..2f91fab --- /dev/null +++ b/api/src/graphql/title.sdl.ts @@ -0,0 +1,18 @@ +export const schema = gql` + type Titles { + id: Int! + titles: [String]! + } + + type Query { + titles: Titles! @skipAuth + } + + input UpdateTitlesInput { + titles: [String]! + } + + type Mutation { + updateTitles(input: UpdateTitlesInput!): Titles! @requireAuth + } +` diff --git a/api/src/graphql/titles.sdl.ts b/api/src/graphql/titles.sdl.ts deleted file mode 100644 index f5e191d..0000000 --- a/api/src/graphql/titles.sdl.ts +++ /dev/null @@ -1,25 +0,0 @@ -export const schema = gql` - type Title { - id: Int! - title: String! - } - - type Query { - titles: [Title!]! @skipAuth - title(id: Int!): Title @skipAuth - } - - input CreateTitleInput { - title: String! - } - - input UpdateTitleInput { - title: String - } - - type Mutation { - createTitle(input: CreateTitleInput!): Title! @requireAuth - updateTitle(id: Int!, input: UpdateTitleInput!): Title! @requireAuth - deleteTitle(id: Int!): Title! @requireAuth - } -` diff --git a/api/src/services/title/title.ts b/api/src/services/title/title.ts new file mode 100644 index 0000000..d83ff6f --- /dev/null +++ b/api/src/services/title/title.ts @@ -0,0 +1,11 @@ +import type { QueryResolvers, MutationResolvers } from 'types/graphql' + +import { db } from 'src/lib/db' + +export const titles: QueryResolvers['titles'] = () => db.titles.findFirst() + +export const updateTitles: MutationResolvers['updateTitles'] = ({ input }) => + db.titles.update({ + data: input, + where: { id: 1 }, + }) diff --git a/api/src/services/titles/titles.ts b/api/src/services/titles/titles.ts deleted file mode 100644 index a644711..0000000 --- a/api/src/services/titles/titles.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type { QueryResolvers, MutationResolvers } from 'types/graphql' - -import { db } from 'src/lib/db' - -export const titles: QueryResolvers['titles'] = () => db.title.findMany() - -export const title: QueryResolvers['title'] = ({ id }) => - db.title.findUnique({ - where: { id }, - }) - -export const createTitle: MutationResolvers['createTitle'] = ({ input }) => - db.title.create({ - data: input, - }) - -export const updateTitle: MutationResolvers['updateTitle'] = ({ id, input }) => - db.title.update({ - data: input, - where: { id }, - }) - -export const deleteTitle: MutationResolvers['deleteTitle'] = ({ id }) => - db.title.delete({ - where: { id }, - }) diff --git a/scripts/seed.ts b/scripts/seed.ts index 10d8ada..992845f 100644 --- a/scripts/seed.ts +++ b/scripts/seed.ts @@ -3,6 +3,8 @@ import { db } from 'api/src/lib/db' import { hashPassword } from '@redwoodjs/auth-dbauth-api' +const MAX_TITLES = 5 + export default async () => { try { const admin = { @@ -29,6 +31,24 @@ export default async () => { salt, }, }) + + const titles = await db.titles.findFirst() + + await db.titles.upsert({ + where: { + id: 1, + }, + create: { + titles: Array.from({ length: MAX_TITLES }).map( + (_, i) => `a title ${i + 1}` + ), + }, + update: { + titles: + titles?.titles || + Array.from({ length: MAX_TITLES }).map((_, i) => `a title ${i + 1}`), + }, + }) } catch (error) { console.error(error) } diff --git a/web/src/Routes.tsx b/web/src/Routes.tsx index 4595bcf..5205237 100644 --- a/web/src/Routes.tsx +++ b/web/src/Routes.tsx @@ -27,6 +27,10 @@ const Routes = () => { + + + + diff --git a/web/src/components/Portrait/PortraitCell/PortraitCell.tsx b/web/src/components/Portrait/PortraitCell/PortraitCell.tsx index ab35c60..b055544 100644 --- a/web/src/components/Portrait/PortraitCell/PortraitCell.tsx +++ b/web/src/components/Portrait/PortraitCell/PortraitCell.tsx @@ -22,9 +22,7 @@ export const QUERY: TypedDocumentNode = ` export const Loading = () => - export const Empty = () => - export const Failure = ({ error }: CellFailureProps) => ( ) diff --git a/web/src/components/ThemeToggle/ThemeToggle.tsx b/web/src/components/ThemeToggle/ThemeToggle.tsx index 87cb9f0..b5e60a8 100644 --- a/web/src/components/ThemeToggle/ThemeToggle.tsx +++ b/web/src/components/ThemeToggle/ThemeToggle.tsx @@ -3,14 +3,17 @@ import { useState, useEffect } from 'react' import { mdiWeatherSunny, mdiWeatherNight } from '@mdi/js' import Icon from '@mdi/react' +const LIGHT_THEME = 'light' +const DARK_THEME = 'dark' + const ThemeToggle = () => { const [theme, setTheme] = useState( - localStorage.getItem('theme') ? localStorage.getItem('theme') : 'light' + localStorage.getItem('theme') ? localStorage.getItem('theme') : LIGHT_THEME ) const handleToggle = (e: React.ChangeEvent) => { - if (e.target.checked) setTheme('dark') - else setTheme('light') + if (e.target.checked) setTheme(DARK_THEME) + else setTheme(LIGHT_THEME) } useEffect(() => { @@ -31,7 +34,7 @@ const ThemeToggle = () => { diff --git a/web/src/components/Title/AdminTitlesCell/AdminTitlesCell.tsx b/web/src/components/Title/AdminTitlesCell/AdminTitlesCell.tsx new file mode 100644 index 0000000..0232ce3 --- /dev/null +++ b/web/src/components/Title/AdminTitlesCell/AdminTitlesCell.tsx @@ -0,0 +1,50 @@ +import type { AdminTitlesQuery, AdminTitlesQueryVariables } from 'types/graphql' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' + +import CellFailure from 'src/components/Cell/CellFailure/CellFailure' +import CellLoading from 'src/components/Cell/CellLoading/CellLoading' + +import TitlesForm from '../TitlesForm/TitlesForm' + +export const QUERY: TypedDocumentNode< + AdminTitlesQuery, + AdminTitlesQueryVariables +> = gql` + query AdminTitlesQuery { + titles { + id + titles + } + } +` + +export const Loading = () => +export const Failure = ({ + error, +}: CellFailureProps) => + +export const Success = ({ titles }: CellSuccessProps) => ( +
+
+ + + + + + + + + + + +
Titles
+ +
+
+
+) diff --git a/web/src/components/Title/Titles/Titles.tsx b/web/src/components/Title/Titles/Titles.tsx new file mode 100644 index 0000000..cd61a67 --- /dev/null +++ b/web/src/components/Title/Titles/Titles.tsx @@ -0,0 +1,32 @@ +import { ReactTyped } from 'react-typed' + +interface TitlesProps { + titles: string[] + className?: string +} + +export const Titles = ({ titles, className }: TitlesProps) => { + const titlesFiltered = titles.filter((title) => title !== '') + + return ( + <> +

+ Hey 👋, I'm {`${process.env.FIRST_NAME}`} + {titlesFiltered.length > 0 && ( + <> + ,
+ + + )} +

+ + ) +} diff --git a/web/src/components/Title/TitlesCell/TitlesCell.tsx b/web/src/components/Title/TitlesCell/TitlesCell.tsx new file mode 100644 index 0000000..dba669c --- /dev/null +++ b/web/src/components/Title/TitlesCell/TitlesCell.tsx @@ -0,0 +1,45 @@ +import type { TitlesQuery, TitlesQueryVariables } from 'types/graphql' + +import type { + CellSuccessProps, + CellFailureProps, + TypedDocumentNode, +} from '@redwoodjs/web' + +import CellFailure from 'src/components/Cell/CellFailure/CellFailure' +import CellLoading from 'src/components/Cell/CellLoading/CellLoading' + +import { Titles } from '../Titles/Titles' + +export const QUERY: TypedDocumentNode = gql` + query TitlesQuery { + titles { + titles + } + } +` + +interface TitleProps { + className?: string +} + +export const beforeQuery = ({ className = '' }: TitleProps) => { + return { + variables: { + className, + }, + fetchPolicy: 'cache-and-network', + } +} + +export const Loading = () => +export const Failure = ({ error }: CellFailureProps) => ( + +) + +export const Success = ({ + titles: { titles }, + className = '', +}: CellSuccessProps & TitleProps) => ( + +) diff --git a/web/src/components/Title/TitlesForm/TitlesForm.tsx b/web/src/components/Title/TitlesForm/TitlesForm.tsx new file mode 100644 index 0000000..d04c30d --- /dev/null +++ b/web/src/components/Title/TitlesForm/TitlesForm.tsx @@ -0,0 +1,113 @@ +import { useEffect, useRef, useState } from 'react' + +import { mdiFormatTitle } from '@mdi/js' +import Icon from '@mdi/react' +import { Titles, UpdateTitlesInput } from 'types/graphql' + +import { Form, Label, Submit, TextField } from '@redwoodjs/forms' +import { TypedDocumentNode, useMutation } from '@redwoodjs/web' +import { toast } from '@redwoodjs/web/toast' + +const MAX_TITLES = 5 + +interface TitlesFormProps { + titles?: Titles +} + +const UPDATE_TITLES_MUTATION: TypedDocumentNode = gql` + mutation UpdateTitlesMutation($input: UpdateTitlesInput!) { + updateTitles(input: $input) { + titles + } + } +` + +const TitlesForm = ({ titles }: TitlesFormProps) => { + const title1ref = useRef(null) + + const [preview, setPreview] = useState(false) + + const states = [ + useState(titles?.titles[0]), + useState(titles?.titles[1]), + useState(titles?.titles[2]), + useState(titles?.titles[3]), + useState(titles?.titles[4]), + ] + + useEffect(() => title1ref.current?.focus(), []) + + const [updateTitles, { loading: updateLoading }] = useMutation( + UPDATE_TITLES_MUTATION, + { + onCompleted: () => toast.success('Titles saved'), + onError: (error) => toast.error(error.message), + } + ) + + const onSubmit = (data: Record) => + updateTitles({ + variables: { + input: { + titles: Object.values(data).map((value) => value), + }, + }, + }) + + return ( +
+ {Array.from({ length: MAX_TITLES }).map((_, i) => ( + + ))} + + +
+ ) +} + +export default TitlesForm diff --git a/web/src/layouts/NavbarLayout/NavbarLayout.tsx b/web/src/layouts/NavbarLayout/NavbarLayout.tsx index 18313a8..2f862f7 100644 --- a/web/src/layouts/NavbarLayout/NavbarLayout.tsx +++ b/web/src/layouts/NavbarLayout/NavbarLayout.tsx @@ -51,6 +51,10 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => { name: 'Portrait', path: routes.portrait(), }, + { + name: 'Titles', + path: routes.titles(), + }, { name: 'Resume', path: routes.adminResume(), @@ -66,11 +70,9 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => { const navbarAdminButtons = () => navbarAdminRoutes.map((route, i) => ( -
  • - - {route.name} - -
  • + + {route.name} + )) return ( @@ -90,31 +92,24 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
    + {isAuthenticated && ( +

    + Public +

    + )} {navbarButtons()} {isAuthenticated && ( -
    -
    -

    - Admin -

    - - {navbarAdminButtons()} -
  • - -
  • -
    -
    + <> +

    + Admin +

    + {navbarAdminButtons()} + + )}
    @@ -138,7 +133,7 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
    {isAuthenticated && ( -
    +
    @@ -153,7 +148,7 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
      {navbarAdminButtons()}
    diff --git a/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx b/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx index 076bd30..91b8a56 100644 --- a/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx +++ b/web/src/pages/ForgotPasswordPage/ForgotPasswordPage.tsx @@ -72,7 +72,7 @@ const ForgotPasswordPage = () => {
    - Submit + Submit
    diff --git a/web/src/pages/HomePage/HomePage.tsx b/web/src/pages/HomePage/HomePage.tsx index 4b8605b..bc91be5 100644 --- a/web/src/pages/HomePage/HomePage.tsx +++ b/web/src/pages/HomePage/HomePage.tsx @@ -1,9 +1,19 @@ import { Metadata } from '@redwoodjs/web' +import TitlesCell from 'src/components/Title/TitlesCell' + const HomePage = () => { return ( <> + +
    +
    +
    + +
    +
    +
    ) } diff --git a/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx b/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx index 4258eab..cc14b74 100644 --- a/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx +++ b/web/src/pages/ResetPasswordPage/ResetPasswordPage.tsx @@ -102,7 +102,7 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
    ( + <> + + + +) + +export default TitlesPage