Project tags CRUD + fix page metadata

This commit is contained in:
Ahmed Al-Taiar
2024-09-06 22:08:02 -04:00
parent 3204a8319c
commit 1c5a8d026a
42 changed files with 933 additions and 135 deletions

View File

@ -1,4 +1,9 @@
import { URLTypeDefinition, URLResolver } from 'graphql-scalars'
import {
URLTypeDefinition,
URLResolver,
HexColorCodeDefinition,
HexColorCodeResolver,
} from 'graphql-scalars'
import { createAuthDecoder } from '@redwoodjs/auth-dbauth-api'
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
@ -21,13 +26,11 @@ export const handler = createGraphQLHandler({
sdls,
services,
schemaOptions: {
typeDefs: [URLTypeDefinition],
typeDefs: [URLTypeDefinition, HexColorCodeDefinition],
resolvers: {
URL: URLResolver,
HexColorCode: HexColorCodeResolver,
},
},
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()
},
onException: () => db.$disconnect(),
})

View File

@ -1,3 +1,4 @@
export const schema = gql`
scalar URL
scalar HexColorCode
`

View File

@ -2,7 +2,7 @@ export const schema = gql`
type Tag {
id: Int!
tag: String!
color: String!
color: HexColorCode!
projects: [Project]!
}
@ -13,12 +13,12 @@ export const schema = gql`
input CreateTagInput {
tag: String!
color: String!
color: HexColorCode!
}
input UpdateTagInput {
tag: String
color: String
color: HexColorCode
}
type Mutation {

View File

@ -6,37 +6,30 @@ import type {
import { db } from 'src/lib/db'
export const tags: QueryResolvers['tags'] = () => {
return db.tag.findMany()
}
export const tags: QueryResolvers['tags'] = () => db.tag.findMany()
export const tag: QueryResolvers['tag'] = ({ id }) => {
return db.tag.findUnique({
export const tag: QueryResolvers['tag'] = ({ id }) =>
db.tag.findUnique({
where: { id },
})
}
export const createTag: MutationResolvers['createTag'] = ({ input }) => {
return db.tag.create({
export const createTag: MutationResolvers['createTag'] = ({ input }) =>
db.tag.create({
data: input,
})
}
export const updateTag: MutationResolvers['updateTag'] = ({ id, input }) => {
return db.tag.update({
export const updateTag: MutationResolvers['updateTag'] = ({ id, input }) =>
db.tag.update({
data: input,
where: { id },
})
}
export const deleteTag: MutationResolvers['deleteTag'] = ({ id }) => {
return db.tag.delete({
export const deleteTag: MutationResolvers['deleteTag'] = ({ id }) =>
db.tag.delete({
where: { id },
})
}
export const Tag: TagRelationResolvers = {
projects: (_obj, { root }) => {
return db.tag.findUnique({ where: { id: root?.id } }).projects()
},
projects: (_obj, { root }) =>
db.tag.findUnique({ where: { id: root?.id } }).projects(),
}

View File

@ -29,6 +29,7 @@
"@uppy/tus": "^4.0.0",
"humanize-string": "2.1.0",
"react": "18.3.1",
"react-colorful": "^5.6.1",
"react-dom": "18.3.1"
},
"devDependencies": {

View File

@ -9,6 +9,13 @@ const Routes = () => {
return (
<Router useAuth={useAuth}>
<PrivateSet unauthenticated="home">
<Set wrap={ScaffoldLayout} title="Tags" titleTo="tags" buttonLabel="New Tag" buttonTo="newTag">
<Route path="/admin/tags/new" page={TagNewTagPage} name="newTag" />
<Route path="/admin/tags/{id:Int}/edit" page={TagEditTagPage} name="editTag" />
<Route path="/admin/tags/{id:Int}" page={TagTagPage} name="tag" />
<Route path="/admin/tags" page={TagTagsPage} name="tags" />
</Set>
<Set wrap={ScaffoldLayout} title="Socials" titleTo="socials" buttonLabel="New Social" buttonTo="newSocial">
<Route path="/admin/socials/new" page={SocialNewSocialPage} name="newSocial" />
<Route path="/admin/socials/{id:Int}/edit" page={SocialEditSocialPage} name="editSocial" />

View File

@ -0,0 +1,54 @@
import { mdiPound, mdiContentCopy, mdiContentPaste } from '@mdi/js'
import Icon from '@mdi/react'
import { HexColorInput, HexColorPicker } from 'react-colorful'
import { toast } from '@redwoodjs/web/toast'
interface ColorPickerProps {
color: string
setColor: React.Dispatch<React.SetStateAction<string>>
}
const ColorPicker = ({ color, setColor }: ColorPickerProps) => {
return (
<div className="flex flex-col space-y-2 bg-base-100 p-2 rounded-xl w-min">
<section className="w-52">
<HexColorPicker color={color} onChange={setColor} />
</section>
<div className="flex space-x-2 w-52">
{/* eslint-disable-next-line jsx-a11y/label-has-associated-control */}
<label className="input input-bordered flex items-center gap-2 input-sm w-full grow">
<Icon path={mdiPound} className="size-4 opacity-70" />
<HexColorInput color={color} className="w-16" />
</label>
<button
className="btn btn-square btn-sm "
onClick={async () => {
try {
await navigator.clipboard.writeText(color)
toast.success('Copied color to clipboard')
} catch {
toast.error(`Failed to copy, please try again`)
}
}}
>
<Icon path={mdiContentCopy} className="size-4" />
</button>
<button
className="btn btn-square btn-sm "
onClick={async () => {
try {
setColor(await navigator.clipboard.readText())
} catch {
toast.error(`Failed to paste, please try again`)
}
}}
>
<Icon path={mdiContentPaste} className="size-4" />
</button>
</div>
</div>
)
}
export default ColorPicker

View File

@ -20,17 +20,15 @@ interface PortraitFormProps {
portrait?: Portrait
}
export const QUERY: TypedDocumentNode<
FindPortrait,
FindPortraitVariables
> = gql`
query PortraitForm {
portrait {
id
fileId
export const QUERY: TypedDocumentNode<FindPortrait, FindPortraitVariables> =
gql`
query PortraitForm {
portrait {
id
fileId
}
}
}
`
`
const DELETE_PORTRAIT_MUTATION: TypedDocumentNode<
DeletePortraitMutation,

View File

@ -13,6 +13,8 @@ import type {
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
import ProjectForm from 'src/components/Project/ProjectForm'
export const QUERY: TypedDocumentNode<EditProjectById> = gql`
@ -42,10 +44,10 @@ const UPDATE_PROJECT_MUTATION: TypedDocumentNode<
}
`
export const Loading = () => <div>Loading...</div>
export const Loading = () => <CellLoading />
export const Failure = ({ error }: CellFailureProps) => (
<div className="rw-cell-error">{error?.message}</div>
<CellFailure error={error} />
)
export const Success = ({ project }: CellSuccessProps<EditProjectById>) => {

View File

@ -6,6 +6,9 @@ import type {
TypedDocumentNode,
} from '@redwoodjs/web'
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
import Project from 'src/components/Project/Project'
export const QUERY: TypedDocumentNode<
@ -23,18 +26,16 @@ export const QUERY: TypedDocumentNode<
}
`
export const Loading = () => <div>Loading...</div>
export const Loading = () => <CellLoading />
export const Empty = () => <div>Project not found</div>
export const Empty = () => <CellEmpty />
export const Failure = ({
error,
}: CellFailureProps<FindProjectByIdVariables>) => (
<div className="rw-cell-error">{error?.message}</div>
)
}: CellFailureProps<FindProjectByIdVariables>) => <CellFailure error={error} />
export const Success = ({
project,
}: CellSuccessProps<FindProjectById, FindProjectByIdVariables>) => {
return <Project project={project} />
}
}: CellSuccessProps<FindProjectById, FindProjectByIdVariables>) => (
<Project project={project} />
)

View File

@ -1,48 +1,39 @@
import type { FindProjects, FindProjectsVariables } from 'types/graphql'
import { Link, routes } from '@redwoodjs/router'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
import Projects from 'src/components/Project/Projects'
export const QUERY: TypedDocumentNode<
FindProjects,
FindProjectsVariables
> = gql`
query FindProjects {
projects {
id
title
description
date
links
export const QUERY: TypedDocumentNode<FindProjects, FindProjectsVariables> =
gql`
query FindProjects {
projects {
id
title
description
date
links
}
}
}
`
`
export const Loading = () => <div>Loading...</div>
export const Loading = () => <CellLoading />
export const Empty = () => {
return (
<div className="rw-text-center">
{'No projects yet. '}
<Link to={routes.newProject()} className="rw-link">
{'Create one?'}
</Link>
</div>
)
}
export const Empty = () => <CellEmpty />
export const Failure = ({ error }: CellFailureProps<FindProjects>) => (
<div className="rw-cell-error">{error?.message}</div>
<CellFailure error={error} />
)
export const Success = ({
projects,
}: CellSuccessProps<FindProjects, FindProjectsVariables>) => {
return <Projects projects={projects} />
}
}: CellSuccessProps<FindProjects, FindProjectsVariables>) => (
<Projects projects={projects} />
)

View File

@ -30,15 +30,12 @@ const NewSocial = () => {
toast.success('Social created')
navigate(routes.socials())
},
onError: (error) => {
toast.error(error.message)
},
onError: (error) => toast.error(error.message),
}
)
const onSave = (input: CreateSocialInput) => {
const onSave = (input: CreateSocialInput) =>
createSocial({ variables: { input } })
}
return (
<div className="flex w-full justify-center">

View File

@ -11,19 +11,17 @@ import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
import Social from 'src/components/Social/Social'
export const QUERY: TypedDocumentNode<
FindSocialById,
FindSocialByIdVariables
> = gql`
query FindSocialById($id: Int!) {
social: social(id: $id) {
id
name
type
username
export const QUERY: TypedDocumentNode<FindSocialById, FindSocialByIdVariables> =
gql`
query FindSocialById($id: Int!) {
social: social(id: $id) {
id
name
type
username
}
}
}
`
`
export const Loading = () => <CellLoading />

View File

@ -69,6 +69,9 @@ const SocialForm = (props: SocialFormProps) => {
props.onSave(data, props?.social?.id)
}
const nameRef = useRef<HTMLInputElement>(null)
useEffect(() => nameRef.current?.focus(), [])
return (
<Form<FormSocial>
onSubmit={onSubmit}
@ -90,6 +93,7 @@ const SocialForm = (props: SocialFormProps) => {
</Label>
<TextField
name="name"
ref={nameRef}
placeholder="Name"
defaultValue={props.social?.name}
className="w-full"
@ -125,8 +129,8 @@ const SocialForm = (props: SocialFormProps) => {
type == 'email'
? mdiAt
: type == 'custom'
? mdiLinkVariant
: mdiAccount
? mdiLinkVariant
: mdiAccount
}
/>
</Label>

View File

@ -0,0 +1,86 @@
import type {
EditTagById,
UpdateTagInput,
UpdateTagMutationVariables,
} from 'types/graphql'
import { navigate, routes } from '@redwoodjs/router'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import { useMutation } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
import TagForm from 'src/components/Tag/TagForm'
export const QUERY: TypedDocumentNode<EditTagById> = gql`
query EditTagById($id: Int!) {
tag: tag(id: $id) {
id
tag
color
}
}
`
const UPDATE_TAG_MUTATION: TypedDocumentNode<
EditTagById,
UpdateTagMutationVariables
> = gql`
mutation UpdateTagMutation($id: Int!, $input: UpdateTagInput!) {
updateTag(id: $id, input: $input) {
id
tag
color
}
}
`
export const Loading = () => <CellLoading />
export const Failure = ({ error }: CellFailureProps) => (
<CellFailure error={error} />
)
export const Success = ({ tag }: CellSuccessProps<EditTagById>) => {
const [updateTag, { loading, error }] = useMutation(UPDATE_TAG_MUTATION, {
onCompleted: () => {
toast.success('Tag updated')
navigate(routes.tags())
},
onError: (error) => toast.error(error.message),
})
const onSave = (input: UpdateTagInput, id: EditTagById['tag']['id']) =>
updateTag({ variables: { id, input } })
return (
<div className="flex w-full justify-center">
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
<table className="table">
<thead className="bg-base-200 font-syne">
<tr>
<th>Edit Tag {tag?.id}</th>
</tr>
</thead>
<tbody>
<tr>
<th>
<TagForm
tag={tag}
onSave={onSave}
error={error}
loading={loading}
/>
</th>
</tr>
</tbody>
</table>
</div>
</div>
)
}

View File

@ -0,0 +1,58 @@
import type {
CreateTagMutation,
CreateTagInput,
CreateTagMutationVariables,
} from 'types/graphql'
import { navigate, routes } from '@redwoodjs/router'
import { useMutation } from '@redwoodjs/web'
import type { TypedDocumentNode } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import TagForm from 'src/components/Tag/TagForm'
const CREATE_TAG_MUTATION: TypedDocumentNode<
CreateTagMutation,
CreateTagMutationVariables
> = gql`
mutation CreateTagMutation($input: CreateTagInput!) {
createTag(input: $input) {
id
}
}
`
const NewTag = () => {
const [createTag, { loading, error }] = useMutation(CREATE_TAG_MUTATION, {
onCompleted: () => {
toast.success('Tag created')
navigate(routes.tags())
},
onError: (error) => toast.error(error.message),
})
const onSave = (input: CreateTagInput) => createTag({ variables: { input } })
return (
<div className="flex w-full justify-center">
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
<table className="table">
<thead className="bg-base-200 font-syne">
<tr>
<th>New Tag</th>
</tr>
</thead>
<tbody>
<tr>
<th>
<TagForm onSave={onSave} error={error} loading={loading} />
</th>
</tr>
</tbody>
</table>
</div>
</div>
)
}
export default NewTag

View File

@ -0,0 +1,104 @@
import type {
DeleteTagMutation,
DeleteTagMutationVariables,
FindTagById,
} from 'types/graphql'
import { Link, routes, navigate } from '@redwoodjs/router'
import { useMutation } from '@redwoodjs/web'
import type { TypedDocumentNode } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import {} from 'src/lib/formatters'
import { calculateLuminance } from 'src/lib/color'
const DELETE_TAG_MUTATION: TypedDocumentNode<
DeleteTagMutation,
DeleteTagMutationVariables
> = gql`
mutation DeleteTagMutation($id: Int!) {
deleteTag(id: $id) {
id
}
}
`
interface Props {
tag: NonNullable<FindTagById['tag']>
}
const Tag = ({ tag }: Props) => {
const [deleteTag] = useMutation(DELETE_TAG_MUTATION, {
onCompleted: () => {
toast.success('Tag deleted')
navigate(routes.tags())
},
onError: (error) => toast.error(error.message),
})
const onDeleteClick = (id: DeleteTagMutationVariables['id']) => {
if (confirm('Are you sure you want to delete tag ' + id + '?'))
deleteTag({ variables: { id } })
}
return (
<div className="flex w-full justify-center">
<div className="w-80">
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
<table className="table">
<thead className="bg-base-200 font-syne">
<th className="w-0">
Tag {tag.id}: {tag.tag}
</th>
<th>&nbsp;</th>
</thead>
<tbody>
<tr>
<th>ID</th>
<td>{tag.id}</td>
</tr>
<tr>
<th>Tag</th>
<td>{tag.tag}</td>
</tr>
<tr>
<th>Color</th>
<td>
<div
className="badge"
style={{
backgroundColor: tag.color,
color:
calculateLuminance(tag.color) > 0.5 ? 'black' : 'white',
}}
>
{tag.color}
</div>
</td>
</tr>
</tbody>
</table>
</div>
<nav className="my-2 flex justify-center space-x-2">
<Link
to={routes.editTag({ id: tag.id })}
title={'Edit tag ' + tag.id}
className="btn btn-primary btn-sm uppercase"
>
Edit
</Link>
<button
type="button"
title={'Delete tag ' + tag.id}
className="btn btn-error btn-sm uppercase"
onClick={() => onDeleteClick(tag.id)}
>
Delete
</button>
</nav>
</div>
</div>
)
}
export default Tag

View File

@ -0,0 +1,34 @@
import type { FindTagById, FindTagByIdVariables } from 'types/graphql'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
import Tag from 'src/components/Tag/Tag'
export const QUERY: TypedDocumentNode<FindTagById, FindTagByIdVariables> = gql`
query FindTagById($id: Int!) {
tag: tag(id: $id) {
id
tag
color
}
}
`
export const Loading = () => <CellLoading />
export const Empty = () => <CellEmpty />
export const Failure = ({ error }: CellFailureProps<FindTagByIdVariables>) => (
<CellFailure error={error} />
)
export const Success = ({
tag,
}: CellSuccessProps<FindTagById, FindTagByIdVariables>) => <Tag tag={tag} />

View File

@ -0,0 +1,100 @@
import { useEffect, useRef, useState } from 'react'
import { mdiTag } from '@mdi/js'
import Icon from '@mdi/react'
import type { EditTagById, UpdateTagInput } from 'types/graphql'
import type { RWGqlError } from '@redwoodjs/forms'
import { Form, FieldError, Label, TextField, Submit } from '@redwoodjs/forms'
import ColorPicker from 'src/components/ColorPicker/ColorPicker'
import { calculateLuminance } from 'src/lib/color'
type FormTag = NonNullable<EditTagById['tag']>
interface TagFormProps {
tag?: EditTagById['tag']
onSave: (data: UpdateTagInput, id?: FormTag['id']) => void
error: RWGqlError
loading: boolean
}
const TagForm = (props: TagFormProps) => {
const [tag, setTag] = useState<string>(props.tag?.tag ?? '')
const [color, setColor] = useState<string>(props.tag?.color || '#e5e6e6')
const onSubmit = (data: FormTag) => {
data.color = color
props.onSave(data, props?.tag?.id)
}
const tagRef = useRef<HTMLInputElement>(null)
useEffect(() => tagRef.current?.focus(), [])
return (
<Form<FormTag>
onSubmit={onSubmit}
error={props.error}
className="max-w-56 space-y-2"
>
<Label name="tag" className="form-control w-full">
<Label
name="tag"
className="input input-bordered flex items-center gap-2"
errorClassName="input input-bordered flex items-center gap-2 input-error"
>
<Label
name="tag"
className="h-4 w-4 opacity-70"
errorClassName="h-4 w-4 text-error"
>
<Icon path={mdiTag} />
</Label>
<TextField
name="tag"
ref={tagRef}
placeholder="Tag"
defaultValue={tag}
className="w-full"
maxLength={16}
onChange={(e) => setTag(e.target.value.slice(0, 16))}
validation={{
required: {
value: true,
message: 'Required',
},
}}
/>
</Label>
<div className="label">
<FieldError name="tag" className="text-xs font-semibold text-error" />
</div>
</Label>
<ColorPicker color={color} setColor={setColor} />
<div className="flex justify-center py-2">
<div
className={`badge ${tag.length === 0 && 'hidden'}`}
style={{
backgroundColor: color,
color: calculateLuminance(color) > 0.5 ? 'black' : 'white',
}}
>
{tag}
</div>
</div>
<nav className="my-2 flex justify-center space-x-2">
<Submit
disabled={props.loading}
className="btn btn-primary btn-sm uppercase"
>
Save
</Submit>
</nav>
</Form>
)
}
export default TagForm

View File

@ -0,0 +1,191 @@
import { mdiDotsVertical } from '@mdi/js'
import Icon from '@mdi/react'
import type {
DeleteTagMutation,
DeleteTagMutationVariables,
FindTags,
} from 'types/graphql'
import { Link, routes } from '@redwoodjs/router'
import { useMutation } from '@redwoodjs/web'
import type { TypedDocumentNode } from '@redwoodjs/web'
import { toast } from '@redwoodjs/web/toast'
import { QUERY } from 'src/components/Tag/TagsCell'
import { calculateLuminance } from 'src/lib/color'
import { truncate } from 'src/lib/formatters'
const DELETE_TAG_MUTATION: TypedDocumentNode<
DeleteTagMutation,
DeleteTagMutationVariables
> = gql`
mutation DeleteTagMutation($id: Int!) {
deleteTag(id: $id) {
id
}
}
`
const TagsList = ({ tags }: FindTags) => {
const [deleteTag] = useMutation(DELETE_TAG_MUTATION, {
onCompleted: () => toast.success('Tag deleted'),
onError: (error) => toast.error(error.message),
refetchQueries: [{ query: QUERY }],
awaitRefetchQueries: true,
})
const onDeleteClick = (id: DeleteTagMutationVariables['id']) => {
if (confirm('Are you sure you want to delete tag ' + id + '?'))
deleteTag({ variables: { id } })
}
return (
<div className="w-full overflow-hidden overflow-x-auto rounded-xl bg-base-100">
<table className="table">
<thead className="bg-base-200 font-syne">
<tr>
<th className="w-0">Color</th>
<th className="w-1/2">Tag</th>
<th className="w-1/2">Preview</th>
<th className="w-0">&nbsp;</th>
</tr>
</thead>
<tbody>
{tags.map((tag, i) => {
const actionButtons = (
<>
<Link
to={routes.tag({ id: tag.id })}
title={'Show tag ' + tag.id + ' detail'}
className="btn btn-xs uppercase"
>
Show
</Link>
<Link
to={routes.editTag({ id: tag.id })}
title={'Edit social ' + tag.id}
className="btn btn-primary btn-xs uppercase"
>
Edit
</Link>
<button
type="button"
title={'Delete social ' + tag.id}
className="btn btn-error btn-xs uppercase"
onClick={() => onDeleteClick(tag.id)}
>
Delete
</button>
</>
)
return (
<tr key={tag.id}>
<th>
<div
key={i}
className="tooltip tooltip-right"
data-tip={tag.color}
>
<div
className="size-8 rounded-md"
style={{ backgroundColor: tag.color }}
/>
</div>
</th>
<td>{truncate(tag.tag)}</td>
<td>
<div
className="badge"
style={{
backgroundColor: tag.color,
color:
calculateLuminance(tag.color) > 0.5 ? 'black' : 'white',
}}
>
{tag.tag}
</div>
</td>
<td>
<nav className="hidden justify-end space-x-2 sm:flex">
{actionButtons}
</nav>
<div className="dropdown dropdown-end flex justify-end sm:hidden">
<div
tabIndex={0}
role="button"
className="btn btn-square btn-ghost btn-sm lg:hidden"
>
<Icon
path={mdiDotsVertical}
className="text-base-content-100 size-6"
/>
</div>
<div
// eslint-disable-next-line jsx-a11y/no-noninteractive-tabindex
tabIndex={0}
className="menu dropdown-content z-10 -mt-1 mr-10 inline rounded-box bg-base-100 shadow-xl"
>
<nav className="w-46 space-x-2">{actionButtons}</nav>
</div>
</div>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)
// return (
// <div className="rw-segment rw-table-wrapper-responsive">
// <table className="rw-table">
// <thead>
// <tr>
// <th>Id</th>
// <th>Tag</th>
// <th>Color</th>
// <th>&nbsp;</th>
// </tr>
// </thead>
// <tbody>
// {tags.map((tag) => (
// <tr key={tag.id}>
// <td>{truncate(tag.id)}</td>
// <td>{truncate(tag.tag)}</td>
// <td>{truncate(tag.color)}</td>
// <td>
// <nav className="rw-table-actions">
// <Link
// to={routes.tag({ id: tag.id })}
// title={'Show tag ' + tag.id + ' detail'}
// className="rw-button rw-button-small"
// >
// Show
// </Link>
// <Link
// to={routes.editTag({ id: tag.id })}
// title={'Edit tag ' + tag.id}
// className="rw-button rw-button-small rw-button-blue"
// >
// Edit
// </Link>
// <button
// type="button"
// title={'Delete tag ' + tag.id}
// className="rw-button rw-button-small rw-button-red"
// onClick={() => onDeleteClick(tag.id)}
// >
// Delete
// </button>
// </nav>
// </td>
// </tr>
// ))}
// </tbody>
// </table>
// </div>
// )
}
export default TagsList

View File

@ -0,0 +1,31 @@
import type { FindTags, FindTagsVariables } from 'types/graphql'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
import Tags from 'src/components/Tag/Tags'
export const QUERY: TypedDocumentNode<FindTags, FindTagsVariables> = gql`
query FindTags {
tags {
id
tag
color
}
}
`
export const Loading = () => <CellLoading />
export const Empty = () => <CellEmpty />
export const Failure = ({ error }: CellFailureProps<FindTags>) => (
<CellFailure error={error} />
)
export const Success = ({
tags,
}: CellSuccessProps<FindTags, FindTagsVariables>) => <Tags tags={tags} />

View File

@ -11,3 +11,7 @@
/**
* END --- SETUP TAILWINDCSS EDIT
*/
.w-52 .react-colorful {
width: 13rem;
}

View File

@ -31,6 +31,14 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
name: 'Socials',
path: routes.socials(),
},
{
name: 'Projects',
path: routes.projects(),
},
{
name: 'Tags',
path: routes.tags(),
},
{
name: 'Portrait',
path: routes.portrait(),

12
web/src/lib/color.tsx Normal file
View File

@ -0,0 +1,12 @@
export function calculateLuminance(hex: string) {
const r = parseInt(hex.slice(1, 3), 16) / 255
const g = parseInt(hex.slice(3, 5), 16) / 255
const b = parseInt(hex.slice(5, 7), 16) / 255
const luminance =
0.2126 * (r <= 0.03928 ? r / 12.92 : Math.pow((r + 0.055) / 1.055, 2.4)) +
0.7152 * (g <= 0.03928 ? g / 12.92 : Math.pow((g + 0.055) / 1.055, 2.4)) +
0.0722 * (b <= 0.03928 ? b / 12.92 : Math.pow((b + 0.055) / 1.055, 2.4))
return luminance
}

View File

@ -30,7 +30,7 @@ const ContactPage = () => {
return (
<>
<Metadata title={`${process.env.NAME} | Contact`} />
<Metadata title="Contact" />
<div className="flex min-h-[calc(100vh-6rem)] items-center justify-center">
<ContactCardCell />

View File

@ -3,7 +3,7 @@ import { Metadata } from '@redwoodjs/web'
const HomePage = () => {
return (
<>
<Metadata title="Home" description="Home page" />
<Metadata title="Home" />
</>
)
}

View File

@ -24,10 +24,8 @@ const LoginPage = () => {
if (isAuthenticated) navigate(routes.home())
}, [isAuthenticated])
const emailRef = useRef<HTMLInputElement>(null)
useEffect(() => {
emailRef.current?.focus()
}, [])
const usernameRef = useRef<HTMLInputElement>(null)
useEffect(() => usernameRef.current?.focus(), [])
const onSubmit = async (data: Record<string, string>) => {
const response = await logIn({
@ -53,13 +51,14 @@ const LoginPage = () => {
>
<Label
name="username"
className="h-4 w-4 opacity-70"
errorClassName="h-4 w-4 text-error"
className="size-4 opacity-70"
errorClassName="size-4 text-error"
>
<Icon path={mdiAccount} />
</Label>
<TextField
name="username"
ref={usernameRef}
className="grow"
placeholder="Username"
validation={{
@ -79,8 +78,8 @@ const LoginPage = () => {
>
<Label
name="password"
className="h-4 w-4 opacity-70"
errorClassName="h-4 w-4 text-error"
className="size-4 opacity-70"
errorClassName="size-4 text-error"
>
<Icon path={mdiKey} />
</Label>
@ -104,7 +103,7 @@ const LoginPage = () => {
<FieldError name="password" className="text-sm text-error" />
<div className="flex w-full">
<Submit className="btn btn-primary mx-auto">Log In</Submit>
<Submit className="btn btn-primary btn-sm mx-auto">Log In</Submit>
</div>
</Form>
</div>

View File

@ -1,12 +1,17 @@
import { Link, routes } from '@redwoodjs/router'
import { Metadata } from '@redwoodjs/web'
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
export default () => (
<div className="flex h-screen items-center justify-center space-x-2 font-inter">
<Link to={routes.home()} className="btn btn-primary">
404
</Link>
<ThemeToggle />
</div>
<>
<Metadata title="404" />
<div className="flex h-screen items-center justify-center space-x-2 font-inter">
<Link to={routes.home()} className="btn btn-primary">
404
</Link>
<ThemeToggle />
</div>
</>
)

View File

@ -1,5 +1,12 @@
import { Metadata } from '@redwoodjs/web'
import PortraitCell from 'src/components/Portrait/PortraitCell'
const PortraitPage = () => <PortraitCell />
const PortraitPage = () => (
<>
<Metadata title="Portrait" />
<PortraitCell />
</>
)
export default PortraitPage

View File

@ -1,11 +1,16 @@
import { Metadata } from '@redwoodjs/web'
import EditProjectCell from 'src/components/Project/EditProjectCell'
type ProjectPageProps = {
id: number
}
const EditProjectPage = ({ id }: ProjectPageProps) => {
return <EditProjectCell id={id} />
}
const EditProjectPage = ({ id }: ProjectPageProps) => (
<>
<Metadata title={`Edit Project ${id}`} />
<EditProjectCell id={id} />
</>
)
export default EditProjectPage

View File

@ -1,7 +1,12 @@
import { Metadata } from '@redwoodjs/web'
import NewProject from 'src/components/Project/NewProject'
const NewProjectPage = () => {
return <NewProject />
}
const NewProjectPage = () => (
<>
<Metadata title="New Project" />
<NewProject />
</>
)
export default NewProjectPage

View File

@ -1,11 +1,16 @@
import { Metadata } from '@redwoodjs/web'
import ProjectCell from 'src/components/Project/ProjectCell'
type ProjectPageProps = {
id: number
}
const ProjectPage = ({ id }: ProjectPageProps) => {
return <ProjectCell id={id} />
}
const ProjectPage = ({ id }: ProjectPageProps) => (
<>
<Metadata title={`Project ${id}`} />
<ProjectCell id={id} />
</>
)
export default ProjectPage

View File

@ -1,7 +1,12 @@
import { Metadata } from '@redwoodjs/web'
import ProjectsCell from 'src/components/Project/ProjectsCell'
const ProjectsPage = () => {
return <ProjectsCell />
}
const ProjectsPage = () => (
<>
<Metadata title="Projects" />
<ProjectsCell />
</>
)
export default ProjectsPage

View File

@ -1,11 +1,16 @@
import { Metadata } from '@redwoodjs/web'
import EditSocialCell from 'src/components/Social/EditSocialCell'
type SocialPageProps = {
id: number
}
const EditSocialPage = ({ id }: SocialPageProps) => {
return <EditSocialCell id={id} />
}
const EditSocialPage = ({ id }: SocialPageProps) => (
<>
<Metadata title={`Edit Social ${id}`} />
<EditSocialCell id={id} />
</>
)
export default EditSocialPage

View File

@ -1,5 +1,12 @@
import { Metadata } from '@redwoodjs/web'
import NewSocial from 'src/components/Social/NewSocial/NewSocial'
const NewSocialPage = () => <NewSocial />
const NewSocialPage = () => (
<>
<Metadata title="New Social" />
<NewSocial />
</>
)
export default NewSocialPage

View File

@ -1,11 +1,16 @@
import { Metadata } from '@redwoodjs/web'
import SocialCell from 'src/components/Social/SocialCell'
type SocialPageProps = {
id: number
}
const SocialPage = ({ id }: SocialPageProps) => {
return <SocialCell id={id} />
}
const SocialPage = ({ id }: SocialPageProps) => (
<>
<Metadata title={`Social ${id}`} />
<SocialCell id={id} />
</>
)
export default SocialPage

View File

@ -1,7 +1,12 @@
import { Metadata } from '@redwoodjs/web'
import SocialsCell from 'src/components/Social/SocialsCell'
const SocialsPage = () => {
return <SocialsCell />
}
const SocialsPage = () => (
<>
<Metadata title="Socials" />
<SocialsCell />
</>
)
export default SocialsPage

View File

@ -0,0 +1,16 @@
import { Metadata } from '@redwoodjs/web'
import EditTagCell from 'src/components/Tag/EditTagCell'
type TagPageProps = {
id: number
}
const EditTagPage = ({ id }: TagPageProps) => (
<>
<Metadata title={`Edit Tag ${id}`} />
<EditTagCell id={id} />
</>
)
export default EditTagPage

View File

@ -0,0 +1,12 @@
import { Metadata } from '@redwoodjs/web'
import NewTag from 'src/components/Tag/NewTag'
const NewTagPage = () => (
<>
<Metadata title="New Tag" />
<NewTag />
</>
)
export default NewTagPage

View File

@ -0,0 +1,16 @@
import { Metadata } from '@redwoodjs/web'
import TagCell from 'src/components/Tag/TagCell'
type TagPageProps = {
id: number
}
const TagPage = ({ id }: TagPageProps) => (
<>
<Metadata title={`Tag ${id}`} />
<TagCell id={id} />
</>
)
export default TagPage

View File

@ -0,0 +1,12 @@
import { Metadata } from '@redwoodjs/web'
import TagsCell from 'src/components/Tag/TagsCell'
const TagsPage = () => (
<>
<Metadata title="Tags" />
<TagsCell />
</>
)
export default TagsPage

View File

@ -15548,6 +15548,16 @@ __metadata:
languageName: node
linkType: hard
"react-colorful@npm:^5.6.1":
version: 5.6.1
resolution: "react-colorful@npm:5.6.1"
peerDependencies:
react: ">=16.8.0"
react-dom: ">=16.8.0"
checksum: 10c0/48eb73cf71e10841c2a61b6b06ab81da9fffa9876134c239bfdebcf348ce2a47e56b146338e35dfb03512c85966bfc9a53844fc56bc50154e71f8daee59ff6f0
languageName: node
linkType: hard
"react-dom@npm:18.3.1":
version: 18.3.1
resolution: "react-dom@npm:18.3.1"
@ -18237,6 +18247,7 @@ __metadata:
postcss: "npm:^8.4.41"
postcss-loader: "npm:^8.1.1"
react: "npm:18.3.1"
react-colorful: "npm:^5.6.1"
react-dom: "npm:18.3.1"
tailwindcss: "npm:^3.4.8"
languageName: unknown