Titles with a cool effect
This commit is contained in:
16
api/db/migrations/20241005014130_/migration.sql
Normal file
16
api/db/migrations/20241005014130_/migration.sql
Normal file
@ -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")
|
||||||
|
);
|
@ -63,9 +63,9 @@ model Resume {
|
|||||||
fileId String
|
fileId String
|
||||||
}
|
}
|
||||||
|
|
||||||
model Title {
|
model Titles {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
title String
|
titles String[] @default([])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Tag {
|
model Tag {
|
||||||
|
18
api/src/graphql/title.sdl.ts
Normal file
18
api/src/graphql/title.sdl.ts
Normal file
@ -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
|
||||||
|
}
|
||||||
|
`
|
@ -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
|
|
||||||
}
|
|
||||||
`
|
|
11
api/src/services/title/title.ts
Normal file
11
api/src/services/title/title.ts
Normal file
@ -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 },
|
||||||
|
})
|
@ -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 },
|
|
||||||
})
|
|
@ -3,6 +3,8 @@ import { db } from 'api/src/lib/db'
|
|||||||
|
|
||||||
import { hashPassword } from '@redwoodjs/auth-dbauth-api'
|
import { hashPassword } from '@redwoodjs/auth-dbauth-api'
|
||||||
|
|
||||||
|
const MAX_TITLES = 5
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
try {
|
try {
|
||||||
const admin = {
|
const admin = {
|
||||||
@ -29,6 +31,24 @@ export default async () => {
|
|||||||
salt,
|
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) {
|
} catch (error) {
|
||||||
console.error(error)
|
console.error(error)
|
||||||
}
|
}
|
||||||
|
@ -27,6 +27,10 @@ const Routes = () => {
|
|||||||
<Route path="/admin/portrait" page={PortraitPortraitPage} name="portrait" />
|
<Route path="/admin/portrait" page={PortraitPortraitPage} name="portrait" />
|
||||||
</Set>
|
</Set>
|
||||||
|
|
||||||
|
<Set wrap={ScaffoldLayout} title="Titles" titleTo="titles">
|
||||||
|
<Route path="/admin/titles" page={TitleTitlesPage} name="titles" />
|
||||||
|
</Set>
|
||||||
|
|
||||||
<Set wrap={ScaffoldLayout} title="Resume" titleTo="adminResume">
|
<Set wrap={ScaffoldLayout} title="Resume" titleTo="adminResume">
|
||||||
<Route path="/admin/resume" page={ResumeAdminResumePage} name="adminResume" />
|
<Route path="/admin/resume" page={ResumeAdminResumePage} name="adminResume" />
|
||||||
</Set>
|
</Set>
|
||||||
|
@ -22,9 +22,7 @@ export const QUERY: TypedDocumentNode<FindPortrait, FindPortraitVariables> =
|
|||||||
`
|
`
|
||||||
|
|
||||||
export const Loading = () => <CellLoading />
|
export const Loading = () => <CellLoading />
|
||||||
|
|
||||||
export const Empty = () => <CellEmpty />
|
export const Empty = () => <CellEmpty />
|
||||||
|
|
||||||
export const Failure = ({ error }: CellFailureProps<FindPortraitVariables>) => (
|
export const Failure = ({ error }: CellFailureProps<FindPortraitVariables>) => (
|
||||||
<CellFailure error={error} />
|
<CellFailure error={error} />
|
||||||
)
|
)
|
||||||
|
@ -3,14 +3,17 @@ import { useState, useEffect } from 'react'
|
|||||||
import { mdiWeatherSunny, mdiWeatherNight } from '@mdi/js'
|
import { mdiWeatherSunny, mdiWeatherNight } from '@mdi/js'
|
||||||
import Icon from '@mdi/react'
|
import Icon from '@mdi/react'
|
||||||
|
|
||||||
|
const LIGHT_THEME = 'light'
|
||||||
|
const DARK_THEME = 'dark'
|
||||||
|
|
||||||
const ThemeToggle = () => {
|
const ThemeToggle = () => {
|
||||||
const [theme, setTheme] = useState(
|
const [theme, setTheme] = useState(
|
||||||
localStorage.getItem('theme') ? localStorage.getItem('theme') : 'light'
|
localStorage.getItem('theme') ? localStorage.getItem('theme') : LIGHT_THEME
|
||||||
)
|
)
|
||||||
|
|
||||||
const handleToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (e.target.checked) setTheme('dark')
|
if (e.target.checked) setTheme(DARK_THEME)
|
||||||
else setTheme('light')
|
else setTheme(LIGHT_THEME)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -31,7 +34,7 @@ const ThemeToggle = () => {
|
|||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
className="theme-controller"
|
className="theme-controller"
|
||||||
checked={theme === 'dark'}
|
checked={theme === DARK_THEME}
|
||||||
onChange={handleToggle}
|
onChange={handleToggle}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
50
web/src/components/Title/AdminTitlesCell/AdminTitlesCell.tsx
Normal file
50
web/src/components/Title/AdminTitlesCell/AdminTitlesCell.tsx
Normal file
@ -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 = () => <CellLoading />
|
||||||
|
export const Failure = ({
|
||||||
|
error,
|
||||||
|
}: CellFailureProps<AdminTitlesQueryVariables>) => <CellFailure error={error} />
|
||||||
|
|
||||||
|
export const Success = ({ titles }: CellSuccessProps<AdminTitlesQuery>) => (
|
||||||
|
<div className="flex w-full justify-center">
|
||||||
|
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
|
||||||
|
<table className="table w-80">
|
||||||
|
<thead className="bg-base-200 font-syne">
|
||||||
|
<tr>
|
||||||
|
<th className="w-0">Titles</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<th>
|
||||||
|
<TitlesForm titles={titles} />
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
32
web/src/components/Title/Titles/Titles.tsx
Normal file
32
web/src/components/Title/Titles/Titles.tsx
Normal file
@ -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 (
|
||||||
|
<>
|
||||||
|
<h1 className="text-3xl sm:text-5xl font-bold">
|
||||||
|
Hey 👋, I'm {`${process.env.FIRST_NAME}`}
|
||||||
|
{titlesFiltered.length > 0 && (
|
||||||
|
<>
|
||||||
|
, <br />
|
||||||
|
<ReactTyped
|
||||||
|
className={className}
|
||||||
|
strings={titlesFiltered}
|
||||||
|
typeSpeed={50}
|
||||||
|
backSpeed={40}
|
||||||
|
backDelay={1000}
|
||||||
|
startWhenVisible
|
||||||
|
loop
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</h1>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
45
web/src/components/Title/TitlesCell/TitlesCell.tsx
Normal file
45
web/src/components/Title/TitlesCell/TitlesCell.tsx
Normal file
@ -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<TitlesQuery, TitlesQueryVariables> = gql`
|
||||||
|
query TitlesQuery {
|
||||||
|
titles {
|
||||||
|
titles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
interface TitleProps {
|
||||||
|
className?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const beforeQuery = ({ className = '' }: TitleProps) => {
|
||||||
|
return {
|
||||||
|
variables: {
|
||||||
|
className,
|
||||||
|
},
|
||||||
|
fetchPolicy: 'cache-and-network',
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Loading = () => <CellLoading />
|
||||||
|
export const Failure = ({ error }: CellFailureProps<TitlesQueryVariables>) => (
|
||||||
|
<CellFailure error={error} />
|
||||||
|
)
|
||||||
|
|
||||||
|
export const Success = ({
|
||||||
|
titles: { titles },
|
||||||
|
className = '',
|
||||||
|
}: CellSuccessProps<TitlesQuery> & TitleProps) => (
|
||||||
|
<Titles className={className} titles={titles} />
|
||||||
|
)
|
113
web/src/components/Title/TitlesForm/TitlesForm.tsx
Normal file
113
web/src/components/Title/TitlesForm/TitlesForm.tsx
Normal file
@ -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<UpdateTitlesInput> = gql`
|
||||||
|
mutation UpdateTitlesMutation($input: UpdateTitlesInput!) {
|
||||||
|
updateTitles(input: $input) {
|
||||||
|
titles
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
const TitlesForm = ({ titles }: TitlesFormProps) => {
|
||||||
|
const title1ref = useRef<HTMLInputElement>(null)
|
||||||
|
|
||||||
|
const [preview, setPreview] = useState<boolean>(false)
|
||||||
|
|
||||||
|
const states = [
|
||||||
|
useState<string>(titles?.titles[0]),
|
||||||
|
useState<string>(titles?.titles[1]),
|
||||||
|
useState<string>(titles?.titles[2]),
|
||||||
|
useState<string>(titles?.titles[3]),
|
||||||
|
useState<string>(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<string, string>) =>
|
||||||
|
updateTitles({
|
||||||
|
variables: {
|
||||||
|
input: {
|
||||||
|
titles: Object.values(data).map((value) => value),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Form onSubmit={onSubmit} className="max-w-80 space-y-2">
|
||||||
|
{Array.from({ length: MAX_TITLES }).map((_, i) => (
|
||||||
|
<Label key={i} name={`title${i}`} className="form-control w-full">
|
||||||
|
<Label
|
||||||
|
name={`title${i}`}
|
||||||
|
className="input input-bordered flex items-center gap-2"
|
||||||
|
errorClassName="input input-bordered flex items-center gap-2 input-error"
|
||||||
|
>
|
||||||
|
<Label
|
||||||
|
name={`title${i}`}
|
||||||
|
className="size-4 opacity-70"
|
||||||
|
errorClassName="size-4 text-error"
|
||||||
|
>
|
||||||
|
<Icon path={mdiFormatTitle} />
|
||||||
|
</Label>
|
||||||
|
<TextField
|
||||||
|
name={`title${i}`}
|
||||||
|
ref={i === 0 ? title1ref : null}
|
||||||
|
placeholder={`Title ${i + 1}`}
|
||||||
|
defaultValue={states[i][0]}
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={(e) => states[i][1](e.target.value)}
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</Label>
|
||||||
|
{preview && (
|
||||||
|
<div className="label">
|
||||||
|
<p>
|
||||||
|
Hey 👋, I'm {`${process.env.FIRST_NAME}`},{' '}
|
||||||
|
<span className="text-primary">{states[i][0]}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</Label>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<nav className="my-2 flex justify-center space-x-2">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-sm"
|
||||||
|
onClick={() => setPreview(!preview)}
|
||||||
|
>
|
||||||
|
{preview ? 'Hide' : 'Show'} Preview
|
||||||
|
</button>
|
||||||
|
<Submit
|
||||||
|
disabled={updateLoading}
|
||||||
|
className="btn btn-primary btn-sm uppercase"
|
||||||
|
>
|
||||||
|
Save
|
||||||
|
</Submit>
|
||||||
|
</nav>
|
||||||
|
</Form>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TitlesForm
|
@ -51,6 +51,10 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
|
|||||||
name: 'Portrait',
|
name: 'Portrait',
|
||||||
path: routes.portrait(),
|
path: routes.portrait(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Titles',
|
||||||
|
path: routes.titles(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Resume',
|
name: 'Resume',
|
||||||
path: routes.adminResume(),
|
path: routes.adminResume(),
|
||||||
@ -66,11 +70,9 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
|
|||||||
|
|
||||||
const navbarAdminButtons = () =>
|
const navbarAdminButtons = () =>
|
||||||
navbarAdminRoutes.map((route, i) => (
|
navbarAdminRoutes.map((route, i) => (
|
||||||
<li key={i}>
|
<Link key={i} to={route.path} className="btn btn-ghost btn-sm">
|
||||||
<Link to={route.path} className="btn btn-ghost btn-sm">
|
{route.name}
|
||||||
{route.name}
|
</Link>
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
))
|
))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -90,31 +92,24 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
|
|||||||
<div
|
<div
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="menu dropdown-content -ml-2 mt-4 w-36 space-y-2 rounded-box bg-base-200 shadow-xl last:space-y-0"
|
className="menu dropdown-content -ml-2 mt-4 w-36 gap-2 rounded-box bg-base-200 shadow-xl"
|
||||||
>
|
>
|
||||||
|
{isAuthenticated && (
|
||||||
|
<p className="btn btn-active no-animation btn-sm btn-block">
|
||||||
|
Public
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{navbarButtons()}
|
{navbarButtons()}
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<div className="dropdown sm:hidden">
|
<>
|
||||||
<div
|
<p className="btn btn-active no-animation btn-sm btn-block">
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
Admin
|
||||||
tabIndex={0}
|
</p>
|
||||||
className="menu dropdown-content -ml-2 mt-4 w-36 space-y-2 rounded-box bg-base-200 shadow-xl"
|
{navbarAdminButtons()}
|
||||||
>
|
<button onClick={logOut} className="btn btn-error btn-sm">
|
||||||
<p className="btn btn-active no-animation btn-sm btn-block">
|
Logout
|
||||||
Admin
|
</button>
|
||||||
</p>
|
</>
|
||||||
|
|
||||||
{navbarAdminButtons()}
|
|
||||||
<li>
|
|
||||||
<button
|
|
||||||
onClick={logOut}
|
|
||||||
className="btn btn-ghost btn-sm"
|
|
||||||
>
|
|
||||||
Logout
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -138,7 +133,7 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
|
|||||||
</div>
|
</div>
|
||||||
<div className="navbar-end space-x-2">
|
<div className="navbar-end space-x-2">
|
||||||
{isAuthenticated && (
|
{isAuthenticated && (
|
||||||
<div className="hidden space-x-2 sm:flex">
|
<div className="hidden space-x-2 lg:flex">
|
||||||
<button className="btn btn-square btn-ghost" onClick={logOut}>
|
<button className="btn btn-square btn-ghost" onClick={logOut}>
|
||||||
<Icon path={mdiLogout} className="size-8" />
|
<Icon path={mdiLogout} className="size-8" />
|
||||||
</button>
|
</button>
|
||||||
@ -153,7 +148,7 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
|
|||||||
<ul
|
<ul
|
||||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="menu dropdown-content -ml-8 mt-4 w-36 space-y-2 rounded-box bg-base-200 shadow-xl"
|
className="menu dropdown-content -ml-8 mt-4 w-36 gap-2 rounded-box bg-base-200 shadow-xl"
|
||||||
>
|
>
|
||||||
{navbarAdminButtons()}
|
{navbarAdminButtons()}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -72,7 +72,7 @@ const ForgotPasswordPage = () => {
|
|||||||
<FieldError name="username" className="text-sm text-error" />
|
<FieldError name="username" className="text-sm text-error" />
|
||||||
|
|
||||||
<div className="flex w-full">
|
<div className="flex w-full">
|
||||||
<Submit className="btn btn-primary mx-auto">Submit</Submit>
|
<Submit className="btn btn-primary btn-sm mx-auto">Submit</Submit>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,9 +1,19 @@
|
|||||||
import { Metadata } from '@redwoodjs/web'
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
|
import TitlesCell from 'src/components/Title/TitlesCell'
|
||||||
|
|
||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Metadata title="Home" />
|
<Metadata title="Home" />
|
||||||
|
|
||||||
|
<div className="hero min-h-64">
|
||||||
|
<div className="hero-content">
|
||||||
|
<div className="max-w-xl text-center">
|
||||||
|
<TitlesCell className="text-primary" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -102,7 +102,7 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
|
|||||||
|
|
||||||
<div className="flex w-full">
|
<div className="flex w-full">
|
||||||
<Submit
|
<Submit
|
||||||
className={`btn btn-primary mx-auto ${
|
className={`btn btn-primary btn-sm mx-auto ${
|
||||||
!enabled ? 'btn-disabled' : ''
|
!enabled ? 'btn-disabled' : ''
|
||||||
}`}
|
}`}
|
||||||
disabled={!enabled}
|
disabled={!enabled}
|
||||||
|
12
web/src/pages/Title/TitlesPage/TitlesPage.tsx
Normal file
12
web/src/pages/Title/TitlesPage/TitlesPage.tsx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
|
import AdminTitlesCell from 'src/components/Title/AdminTitlesCell'
|
||||||
|
|
||||||
|
const TitlesPage = () => (
|
||||||
|
<>
|
||||||
|
<Metadata title="Portrait" />
|
||||||
|
<AdminTitlesCell />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default TitlesPage
|
Reference in New Issue
Block a user