Project tags CRUD + fix page metadata
This commit is contained in:
@ -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 { createAuthDecoder } from '@redwoodjs/auth-dbauth-api'
|
||||||
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
|
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
|
||||||
@ -21,13 +26,11 @@ export const handler = createGraphQLHandler({
|
|||||||
sdls,
|
sdls,
|
||||||
services,
|
services,
|
||||||
schemaOptions: {
|
schemaOptions: {
|
||||||
typeDefs: [URLTypeDefinition],
|
typeDefs: [URLTypeDefinition, HexColorCodeDefinition],
|
||||||
resolvers: {
|
resolvers: {
|
||||||
URL: URLResolver,
|
URL: URLResolver,
|
||||||
|
HexColorCode: HexColorCodeResolver,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
onException: () => {
|
onException: () => db.$disconnect(),
|
||||||
// Disconnect from your database with an unhandled exception.
|
|
||||||
db.$disconnect()
|
|
||||||
},
|
|
||||||
})
|
})
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
export const schema = gql`
|
export const schema = gql`
|
||||||
scalar URL
|
scalar URL
|
||||||
|
scalar HexColorCode
|
||||||
`
|
`
|
||||||
|
@ -2,7 +2,7 @@ export const schema = gql`
|
|||||||
type Tag {
|
type Tag {
|
||||||
id: Int!
|
id: Int!
|
||||||
tag: String!
|
tag: String!
|
||||||
color: String!
|
color: HexColorCode!
|
||||||
projects: [Project]!
|
projects: [Project]!
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -13,12 +13,12 @@ export const schema = gql`
|
|||||||
|
|
||||||
input CreateTagInput {
|
input CreateTagInput {
|
||||||
tag: String!
|
tag: String!
|
||||||
color: String!
|
color: HexColorCode!
|
||||||
}
|
}
|
||||||
|
|
||||||
input UpdateTagInput {
|
input UpdateTagInput {
|
||||||
tag: String
|
tag: String
|
||||||
color: String
|
color: HexColorCode
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
|
@ -6,37 +6,30 @@ import type {
|
|||||||
|
|
||||||
import { db } from 'src/lib/db'
|
import { db } from 'src/lib/db'
|
||||||
|
|
||||||
export const tags: QueryResolvers['tags'] = () => {
|
export const tags: QueryResolvers['tags'] = () => db.tag.findMany()
|
||||||
return db.tag.findMany()
|
|
||||||
}
|
|
||||||
|
|
||||||
export const tag: QueryResolvers['tag'] = ({ id }) => {
|
export const tag: QueryResolvers['tag'] = ({ id }) =>
|
||||||
return db.tag.findUnique({
|
db.tag.findUnique({
|
||||||
where: { id },
|
where: { id },
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
export const createTag: MutationResolvers['createTag'] = ({ input }) => {
|
export const createTag: MutationResolvers['createTag'] = ({ input }) =>
|
||||||
return db.tag.create({
|
db.tag.create({
|
||||||
data: input,
|
data: input,
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
export const updateTag: MutationResolvers['updateTag'] = ({ id, input }) => {
|
export const updateTag: MutationResolvers['updateTag'] = ({ id, input }) =>
|
||||||
return db.tag.update({
|
db.tag.update({
|
||||||
data: input,
|
data: input,
|
||||||
where: { id },
|
where: { id },
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteTag: MutationResolvers['deleteTag'] = ({ id }) => {
|
export const deleteTag: MutationResolvers['deleteTag'] = ({ id }) =>
|
||||||
return db.tag.delete({
|
db.tag.delete({
|
||||||
where: { id },
|
where: { id },
|
||||||
})
|
})
|
||||||
}
|
|
||||||
|
|
||||||
export const Tag: TagRelationResolvers = {
|
export const Tag: TagRelationResolvers = {
|
||||||
projects: (_obj, { root }) => {
|
projects: (_obj, { root }) =>
|
||||||
return db.tag.findUnique({ where: { id: root?.id } }).projects()
|
db.tag.findUnique({ where: { id: root?.id } }).projects(),
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@
|
|||||||
"@uppy/tus": "^4.0.0",
|
"@uppy/tus": "^4.0.0",
|
||||||
"humanize-string": "2.1.0",
|
"humanize-string": "2.1.0",
|
||||||
"react": "18.3.1",
|
"react": "18.3.1",
|
||||||
|
"react-colorful": "^5.6.1",
|
||||||
"react-dom": "18.3.1"
|
"react-dom": "18.3.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -9,6 +9,13 @@ const Routes = () => {
|
|||||||
return (
|
return (
|
||||||
<Router useAuth={useAuth}>
|
<Router useAuth={useAuth}>
|
||||||
<PrivateSet unauthenticated="home">
|
<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">
|
<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/new" page={SocialNewSocialPage} name="newSocial" />
|
||||||
<Route path="/admin/socials/{id:Int}/edit" page={SocialEditSocialPage} name="editSocial" />
|
<Route path="/admin/socials/{id:Int}/edit" page={SocialEditSocialPage} name="editSocial" />
|
||||||
|
54
web/src/components/ColorPicker/ColorPicker.tsx
Normal file
54
web/src/components/ColorPicker/ColorPicker.tsx
Normal 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
|
@ -20,17 +20,15 @@ interface PortraitFormProps {
|
|||||||
portrait?: Portrait
|
portrait?: Portrait
|
||||||
}
|
}
|
||||||
|
|
||||||
export const QUERY: TypedDocumentNode<
|
export const QUERY: TypedDocumentNode<FindPortrait, FindPortraitVariables> =
|
||||||
FindPortrait,
|
gql`
|
||||||
FindPortraitVariables
|
query PortraitForm {
|
||||||
> = gql`
|
portrait {
|
||||||
query PortraitForm {
|
id
|
||||||
portrait {
|
fileId
|
||||||
id
|
}
|
||||||
fileId
|
|
||||||
}
|
}
|
||||||
}
|
`
|
||||||
`
|
|
||||||
|
|
||||||
const DELETE_PORTRAIT_MUTATION: TypedDocumentNode<
|
const DELETE_PORTRAIT_MUTATION: TypedDocumentNode<
|
||||||
DeletePortraitMutation,
|
DeletePortraitMutation,
|
||||||
|
@ -13,6 +13,8 @@ import type {
|
|||||||
import { useMutation } from '@redwoodjs/web'
|
import { useMutation } from '@redwoodjs/web'
|
||||||
import { toast } from '@redwoodjs/web/toast'
|
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'
|
import ProjectForm from 'src/components/Project/ProjectForm'
|
||||||
|
|
||||||
export const QUERY: TypedDocumentNode<EditProjectById> = gql`
|
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) => (
|
export const Failure = ({ error }: CellFailureProps) => (
|
||||||
<div className="rw-cell-error">{error?.message}</div>
|
<CellFailure error={error} />
|
||||||
)
|
)
|
||||||
|
|
||||||
export const Success = ({ project }: CellSuccessProps<EditProjectById>) => {
|
export const Success = ({ project }: CellSuccessProps<EditProjectById>) => {
|
||||||
|
@ -6,6 +6,9 @@ import type {
|
|||||||
TypedDocumentNode,
|
TypedDocumentNode,
|
||||||
} from '@redwoodjs/web'
|
} 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'
|
import Project from 'src/components/Project/Project'
|
||||||
|
|
||||||
export const QUERY: TypedDocumentNode<
|
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 = ({
|
export const Failure = ({
|
||||||
error,
|
error,
|
||||||
}: CellFailureProps<FindProjectByIdVariables>) => (
|
}: CellFailureProps<FindProjectByIdVariables>) => <CellFailure error={error} />
|
||||||
<div className="rw-cell-error">{error?.message}</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const Success = ({
|
export const Success = ({
|
||||||
project,
|
project,
|
||||||
}: CellSuccessProps<FindProjectById, FindProjectByIdVariables>) => {
|
}: CellSuccessProps<FindProjectById, FindProjectByIdVariables>) => (
|
||||||
return <Project project={project} />
|
<Project project={project} />
|
||||||
}
|
)
|
||||||
|
@ -1,48 +1,39 @@
|
|||||||
import type { FindProjects, FindProjectsVariables } from 'types/graphql'
|
import type { FindProjects, FindProjectsVariables } from 'types/graphql'
|
||||||
|
|
||||||
import { Link, routes } from '@redwoodjs/router'
|
|
||||||
import type {
|
import type {
|
||||||
CellSuccessProps,
|
CellSuccessProps,
|
||||||
CellFailureProps,
|
CellFailureProps,
|
||||||
TypedDocumentNode,
|
TypedDocumentNode,
|
||||||
} from '@redwoodjs/web'
|
} 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'
|
import Projects from 'src/components/Project/Projects'
|
||||||
|
|
||||||
export const QUERY: TypedDocumentNode<
|
export const QUERY: TypedDocumentNode<FindProjects, FindProjectsVariables> =
|
||||||
FindProjects,
|
gql`
|
||||||
FindProjectsVariables
|
query FindProjects {
|
||||||
> = gql`
|
projects {
|
||||||
query FindProjects {
|
id
|
||||||
projects {
|
title
|
||||||
id
|
description
|
||||||
title
|
date
|
||||||
description
|
links
|
||||||
date
|
}
|
||||||
links
|
|
||||||
}
|
}
|
||||||
}
|
`
|
||||||
`
|
|
||||||
|
|
||||||
export const Loading = () => <div>Loading...</div>
|
export const Loading = () => <CellLoading />
|
||||||
|
|
||||||
export const Empty = () => {
|
export const Empty = () => <CellEmpty />
|
||||||
return (
|
|
||||||
<div className="rw-text-center">
|
|
||||||
{'No projects yet. '}
|
|
||||||
<Link to={routes.newProject()} className="rw-link">
|
|
||||||
{'Create one?'}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Failure = ({ error }: CellFailureProps<FindProjects>) => (
|
export const Failure = ({ error }: CellFailureProps<FindProjects>) => (
|
||||||
<div className="rw-cell-error">{error?.message}</div>
|
<CellFailure error={error} />
|
||||||
)
|
)
|
||||||
|
|
||||||
export const Success = ({
|
export const Success = ({
|
||||||
projects,
|
projects,
|
||||||
}: CellSuccessProps<FindProjects, FindProjectsVariables>) => {
|
}: CellSuccessProps<FindProjects, FindProjectsVariables>) => (
|
||||||
return <Projects projects={projects} />
|
<Projects projects={projects} />
|
||||||
}
|
)
|
||||||
|
@ -30,15 +30,12 @@ const NewSocial = () => {
|
|||||||
toast.success('Social created')
|
toast.success('Social created')
|
||||||
navigate(routes.socials())
|
navigate(routes.socials())
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => toast.error(error.message),
|
||||||
toast.error(error.message)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
const onSave = (input: CreateSocialInput) => {
|
const onSave = (input: CreateSocialInput) =>
|
||||||
createSocial({ variables: { input } })
|
createSocial({ variables: { input } })
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full justify-center">
|
<div className="flex w-full justify-center">
|
||||||
|
@ -11,19 +11,17 @@ import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
|
|||||||
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
|
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
|
||||||
import Social from 'src/components/Social/Social'
|
import Social from 'src/components/Social/Social'
|
||||||
|
|
||||||
export const QUERY: TypedDocumentNode<
|
export const QUERY: TypedDocumentNode<FindSocialById, FindSocialByIdVariables> =
|
||||||
FindSocialById,
|
gql`
|
||||||
FindSocialByIdVariables
|
query FindSocialById($id: Int!) {
|
||||||
> = gql`
|
social: social(id: $id) {
|
||||||
query FindSocialById($id: Int!) {
|
id
|
||||||
social: social(id: $id) {
|
name
|
||||||
id
|
type
|
||||||
name
|
username
|
||||||
type
|
}
|
||||||
username
|
|
||||||
}
|
}
|
||||||
}
|
`
|
||||||
`
|
|
||||||
|
|
||||||
export const Loading = () => <CellLoading />
|
export const Loading = () => <CellLoading />
|
||||||
|
|
||||||
|
@ -69,6 +69,9 @@ const SocialForm = (props: SocialFormProps) => {
|
|||||||
props.onSave(data, props?.social?.id)
|
props.onSave(data, props?.social?.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const nameRef = useRef<HTMLInputElement>(null)
|
||||||
|
useEffect(() => nameRef.current?.focus(), [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Form<FormSocial>
|
<Form<FormSocial>
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
@ -90,6 +93,7 @@ const SocialForm = (props: SocialFormProps) => {
|
|||||||
</Label>
|
</Label>
|
||||||
<TextField
|
<TextField
|
||||||
name="name"
|
name="name"
|
||||||
|
ref={nameRef}
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
defaultValue={props.social?.name}
|
defaultValue={props.social?.name}
|
||||||
className="w-full"
|
className="w-full"
|
||||||
@ -125,8 +129,8 @@ const SocialForm = (props: SocialFormProps) => {
|
|||||||
type == 'email'
|
type == 'email'
|
||||||
? mdiAt
|
? mdiAt
|
||||||
: type == 'custom'
|
: type == 'custom'
|
||||||
? mdiLinkVariant
|
? mdiLinkVariant
|
||||||
: mdiAccount
|
: mdiAccount
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Label>
|
</Label>
|
||||||
|
86
web/src/components/Tag/EditTagCell/EditTagCell.tsx
Normal file
86
web/src/components/Tag/EditTagCell/EditTagCell.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
58
web/src/components/Tag/NewTag/NewTag.tsx
Normal file
58
web/src/components/Tag/NewTag/NewTag.tsx
Normal 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
|
104
web/src/components/Tag/Tag/Tag.tsx
Normal file
104
web/src/components/Tag/Tag/Tag.tsx
Normal 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> </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
|
34
web/src/components/Tag/TagCell/TagCell.tsx
Normal file
34
web/src/components/Tag/TagCell/TagCell.tsx
Normal 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} />
|
100
web/src/components/Tag/TagForm/TagForm.tsx
Normal file
100
web/src/components/Tag/TagForm/TagForm.tsx
Normal 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
|
191
web/src/components/Tag/Tags/Tags.tsx
Normal file
191
web/src/components/Tag/Tags/Tags.tsx
Normal 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"> </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> </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
|
31
web/src/components/Tag/TagsCell/TagsCell.tsx
Normal file
31
web/src/components/Tag/TagsCell/TagsCell.tsx
Normal 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} />
|
@ -11,3 +11,7 @@
|
|||||||
/**
|
/**
|
||||||
* END --- SETUP TAILWINDCSS EDIT
|
* END --- SETUP TAILWINDCSS EDIT
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
.w-52 .react-colorful {
|
||||||
|
width: 13rem;
|
||||||
|
}
|
||||||
|
@ -31,6 +31,14 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
|
|||||||
name: 'Socials',
|
name: 'Socials',
|
||||||
path: routes.socials(),
|
path: routes.socials(),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'Projects',
|
||||||
|
path: routes.projects(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Tags',
|
||||||
|
path: routes.tags(),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: 'Portrait',
|
name: 'Portrait',
|
||||||
path: routes.portrait(),
|
path: routes.portrait(),
|
||||||
|
12
web/src/lib/color.tsx
Normal file
12
web/src/lib/color.tsx
Normal 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
|
||||||
|
}
|
@ -30,7 +30,7 @@ const ContactPage = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Metadata title={`${process.env.NAME} | Contact`} />
|
<Metadata title="Contact" />
|
||||||
|
|
||||||
<div className="flex min-h-[calc(100vh-6rem)] items-center justify-center">
|
<div className="flex min-h-[calc(100vh-6rem)] items-center justify-center">
|
||||||
<ContactCardCell />
|
<ContactCardCell />
|
||||||
|
@ -3,7 +3,7 @@ import { Metadata } from '@redwoodjs/web'
|
|||||||
const HomePage = () => {
|
const HomePage = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Metadata title="Home" description="Home page" />
|
<Metadata title="Home" />
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -24,10 +24,8 @@ const LoginPage = () => {
|
|||||||
if (isAuthenticated) navigate(routes.home())
|
if (isAuthenticated) navigate(routes.home())
|
||||||
}, [isAuthenticated])
|
}, [isAuthenticated])
|
||||||
|
|
||||||
const emailRef = useRef<HTMLInputElement>(null)
|
const usernameRef = useRef<HTMLInputElement>(null)
|
||||||
useEffect(() => {
|
useEffect(() => usernameRef.current?.focus(), [])
|
||||||
emailRef.current?.focus()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const onSubmit = async (data: Record<string, string>) => {
|
const onSubmit = async (data: Record<string, string>) => {
|
||||||
const response = await logIn({
|
const response = await logIn({
|
||||||
@ -53,13 +51,14 @@ const LoginPage = () => {
|
|||||||
>
|
>
|
||||||
<Label
|
<Label
|
||||||
name="username"
|
name="username"
|
||||||
className="h-4 w-4 opacity-70"
|
className="size-4 opacity-70"
|
||||||
errorClassName="h-4 w-4 text-error"
|
errorClassName="size-4 text-error"
|
||||||
>
|
>
|
||||||
<Icon path={mdiAccount} />
|
<Icon path={mdiAccount} />
|
||||||
</Label>
|
</Label>
|
||||||
<TextField
|
<TextField
|
||||||
name="username"
|
name="username"
|
||||||
|
ref={usernameRef}
|
||||||
className="grow"
|
className="grow"
|
||||||
placeholder="Username"
|
placeholder="Username"
|
||||||
validation={{
|
validation={{
|
||||||
@ -79,8 +78,8 @@ const LoginPage = () => {
|
|||||||
>
|
>
|
||||||
<Label
|
<Label
|
||||||
name="password"
|
name="password"
|
||||||
className="h-4 w-4 opacity-70"
|
className="size-4 opacity-70"
|
||||||
errorClassName="h-4 w-4 text-error"
|
errorClassName="size-4 text-error"
|
||||||
>
|
>
|
||||||
<Icon path={mdiKey} />
|
<Icon path={mdiKey} />
|
||||||
</Label>
|
</Label>
|
||||||
@ -104,7 +103,7 @@ const LoginPage = () => {
|
|||||||
<FieldError name="password" className="text-sm text-error" />
|
<FieldError name="password" className="text-sm text-error" />
|
||||||
|
|
||||||
<div className="flex w-full">
|
<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>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,12 +1,17 @@
|
|||||||
import { Link, routes } from '@redwoodjs/router'
|
import { Link, routes } from '@redwoodjs/router'
|
||||||
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
|
import ThemeToggle from 'src/components/ThemeToggle/ThemeToggle'
|
||||||
|
|
||||||
export default () => (
|
export default () => (
|
||||||
<div className="flex h-screen items-center justify-center space-x-2 font-inter">
|
<>
|
||||||
<Link to={routes.home()} className="btn btn-primary">
|
<Metadata title="404" />
|
||||||
404
|
|
||||||
</Link>
|
<div className="flex h-screen items-center justify-center space-x-2 font-inter">
|
||||||
<ThemeToggle />
|
<Link to={routes.home()} className="btn btn-primary">
|
||||||
</div>
|
404
|
||||||
|
</Link>
|
||||||
|
<ThemeToggle />
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
import PortraitCell from 'src/components/Portrait/PortraitCell'
|
import PortraitCell from 'src/components/Portrait/PortraitCell'
|
||||||
|
|
||||||
const PortraitPage = () => <PortraitCell />
|
const PortraitPage = () => (
|
||||||
|
<>
|
||||||
|
<Metadata title="Portrait" />
|
||||||
|
<PortraitCell />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
export default PortraitPage
|
export default PortraitPage
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
import EditProjectCell from 'src/components/Project/EditProjectCell'
|
import EditProjectCell from 'src/components/Project/EditProjectCell'
|
||||||
|
|
||||||
type ProjectPageProps = {
|
type ProjectPageProps = {
|
||||||
id: number
|
id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditProjectPage = ({ id }: ProjectPageProps) => {
|
const EditProjectPage = ({ id }: ProjectPageProps) => (
|
||||||
return <EditProjectCell id={id} />
|
<>
|
||||||
}
|
<Metadata title={`Edit Project ${id}`} />
|
||||||
|
<EditProjectCell id={id} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
export default EditProjectPage
|
export default EditProjectPage
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
import NewProject from 'src/components/Project/NewProject'
|
import NewProject from 'src/components/Project/NewProject'
|
||||||
|
|
||||||
const NewProjectPage = () => {
|
const NewProjectPage = () => (
|
||||||
return <NewProject />
|
<>
|
||||||
}
|
<Metadata title="New Project" />
|
||||||
|
<NewProject />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
export default NewProjectPage
|
export default NewProjectPage
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
import ProjectCell from 'src/components/Project/ProjectCell'
|
import ProjectCell from 'src/components/Project/ProjectCell'
|
||||||
|
|
||||||
type ProjectPageProps = {
|
type ProjectPageProps = {
|
||||||
id: number
|
id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const ProjectPage = ({ id }: ProjectPageProps) => {
|
const ProjectPage = ({ id }: ProjectPageProps) => (
|
||||||
return <ProjectCell id={id} />
|
<>
|
||||||
}
|
<Metadata title={`Project ${id}`} />
|
||||||
|
<ProjectCell id={id} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
export default ProjectPage
|
export default ProjectPage
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
import ProjectsCell from 'src/components/Project/ProjectsCell'
|
import ProjectsCell from 'src/components/Project/ProjectsCell'
|
||||||
|
|
||||||
const ProjectsPage = () => {
|
const ProjectsPage = () => (
|
||||||
return <ProjectsCell />
|
<>
|
||||||
}
|
<Metadata title="Projects" />
|
||||||
|
<ProjectsCell />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
export default ProjectsPage
|
export default ProjectsPage
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
import EditSocialCell from 'src/components/Social/EditSocialCell'
|
import EditSocialCell from 'src/components/Social/EditSocialCell'
|
||||||
|
|
||||||
type SocialPageProps = {
|
type SocialPageProps = {
|
||||||
id: number
|
id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditSocialPage = ({ id }: SocialPageProps) => {
|
const EditSocialPage = ({ id }: SocialPageProps) => (
|
||||||
return <EditSocialCell id={id} />
|
<>
|
||||||
}
|
<Metadata title={`Edit Social ${id}`} />
|
||||||
|
<EditSocialCell id={id} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
export default EditSocialPage
|
export default EditSocialPage
|
||||||
|
@ -1,5 +1,12 @@
|
|||||||
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
import NewSocial from 'src/components/Social/NewSocial/NewSocial'
|
import NewSocial from 'src/components/Social/NewSocial/NewSocial'
|
||||||
|
|
||||||
const NewSocialPage = () => <NewSocial />
|
const NewSocialPage = () => (
|
||||||
|
<>
|
||||||
|
<Metadata title="New Social" />
|
||||||
|
<NewSocial />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
export default NewSocialPage
|
export default NewSocialPage
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
import SocialCell from 'src/components/Social/SocialCell'
|
import SocialCell from 'src/components/Social/SocialCell'
|
||||||
|
|
||||||
type SocialPageProps = {
|
type SocialPageProps = {
|
||||||
id: number
|
id: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const SocialPage = ({ id }: SocialPageProps) => {
|
const SocialPage = ({ id }: SocialPageProps) => (
|
||||||
return <SocialCell id={id} />
|
<>
|
||||||
}
|
<Metadata title={`Social ${id}`} />
|
||||||
|
<SocialCell id={id} />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
export default SocialPage
|
export default SocialPage
|
||||||
|
@ -1,7 +1,12 @@
|
|||||||
|
import { Metadata } from '@redwoodjs/web'
|
||||||
|
|
||||||
import SocialsCell from 'src/components/Social/SocialsCell'
|
import SocialsCell from 'src/components/Social/SocialsCell'
|
||||||
|
|
||||||
const SocialsPage = () => {
|
const SocialsPage = () => (
|
||||||
return <SocialsCell />
|
<>
|
||||||
}
|
<Metadata title="Socials" />
|
||||||
|
<SocialsCell />
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
|
||||||
export default SocialsPage
|
export default SocialsPage
|
||||||
|
16
web/src/pages/Tag/EditTagPage/EditTagPage.tsx
Normal file
16
web/src/pages/Tag/EditTagPage/EditTagPage.tsx
Normal 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
|
12
web/src/pages/Tag/NewTagPage/NewTagPage.tsx
Normal file
12
web/src/pages/Tag/NewTagPage/NewTagPage.tsx
Normal 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
|
16
web/src/pages/Tag/TagPage/TagPage.tsx
Normal file
16
web/src/pages/Tag/TagPage/TagPage.tsx
Normal 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
|
12
web/src/pages/Tag/TagsPage/TagsPage.tsx
Normal file
12
web/src/pages/Tag/TagsPage/TagsPage.tsx
Normal 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
|
11
yarn.lock
11
yarn.lock
@ -15548,6 +15548,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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":
|
"react-dom@npm:18.3.1":
|
||||||
version: 18.3.1
|
version: 18.3.1
|
||||||
resolution: "react-dom@npm:18.3.1"
|
resolution: "react-dom@npm:18.3.1"
|
||||||
@ -18237,6 +18247,7 @@ __metadata:
|
|||||||
postcss: "npm:^8.4.41"
|
postcss: "npm:^8.4.41"
|
||||||
postcss-loader: "npm:^8.1.1"
|
postcss-loader: "npm:^8.1.1"
|
||||||
react: "npm:18.3.1"
|
react: "npm:18.3.1"
|
||||||
|
react-colorful: "npm:^5.6.1"
|
||||||
react-dom: "npm:18.3.1"
|
react-dom: "npm:18.3.1"
|
||||||
tailwindcss: "npm:^3.4.8"
|
tailwindcss: "npm:^3.4.8"
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
|
Reference in New Issue
Block a user