Projects CRUD (todo: project images)
This commit is contained in:
@ -5,7 +5,7 @@ export const schema = gql`
|
||||
description: String!
|
||||
images: [ProjectImage]!
|
||||
date: DateTime!
|
||||
links: [String]!
|
||||
links: [URL]!
|
||||
tags: [Tag]!
|
||||
}
|
||||
|
||||
@ -18,14 +18,14 @@ export const schema = gql`
|
||||
title: String!
|
||||
description: String!
|
||||
date: DateTime!
|
||||
links: [String]!
|
||||
links: [URL]!
|
||||
}
|
||||
|
||||
input UpdateProjectInput {
|
||||
title: String
|
||||
description: String
|
||||
date: DateTime
|
||||
links: [String]!
|
||||
links: [URL]!
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
|
@ -6,45 +6,35 @@ import type {
|
||||
|
||||
import { db } from 'src/lib/db'
|
||||
|
||||
export const projects: QueryResolvers['projects'] = () => {
|
||||
return db.project.findMany()
|
||||
}
|
||||
export const projects: QueryResolvers['projects'] = () => db.project.findMany()
|
||||
|
||||
export const project: QueryResolvers['project'] = ({ id }) => {
|
||||
return db.project.findUnique({
|
||||
export const project: QueryResolvers['project'] = ({ id }) =>
|
||||
db.project.findUnique({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const createProject: MutationResolvers['createProject'] = ({
|
||||
input,
|
||||
}) => {
|
||||
return db.project.create({
|
||||
export const createProject: MutationResolvers['createProject'] = ({ input }) =>
|
||||
db.project.create({
|
||||
data: input,
|
||||
})
|
||||
}
|
||||
|
||||
export const updateProject: MutationResolvers['updateProject'] = ({
|
||||
id,
|
||||
input,
|
||||
}) => {
|
||||
return db.project.update({
|
||||
}) =>
|
||||
db.project.update({
|
||||
data: input,
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const deleteProject: MutationResolvers['deleteProject'] = ({ id }) => {
|
||||
return db.project.delete({
|
||||
export const deleteProject: MutationResolvers['deleteProject'] = ({ id }) =>
|
||||
db.project.delete({
|
||||
where: { id },
|
||||
})
|
||||
}
|
||||
|
||||
export const Project: ProjectRelationResolvers = {
|
||||
images: (_obj, { root }) => {
|
||||
return db.project.findUnique({ where: { id: root?.id } }).images()
|
||||
},
|
||||
tags: (_obj, { root }) => {
|
||||
return db.project.findUnique({ where: { id: root?.id } }).tags()
|
||||
},
|
||||
images: (_obj, { root }) =>
|
||||
db.project.findUnique({ where: { id: root?.id } }).images(),
|
||||
tags: (_obj, { root }) =>
|
||||
db.project.findUnique({ where: { id: root?.id } }).tags(),
|
||||
}
|
||||
|
@ -22,7 +22,7 @@ const ColorPicker = ({ color, setColor }: ColorPickerProps) => {
|
||||
<HexColorInput color={color} className="w-16" />
|
||||
</label>
|
||||
<button
|
||||
className="btn btn-square btn-sm "
|
||||
className="btn btn-square btn-sm"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(color)
|
||||
|
68
web/src/components/FormTextList/FormTextList.tsx
Normal file
68
web/src/components/FormTextList/FormTextList.tsx
Normal file
@ -0,0 +1,68 @@
|
||||
/* eslint-disable jsx-a11y/label-has-associated-control */
|
||||
import { mdiDelete, mdiPlus } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
|
||||
interface FormTextListProps {
|
||||
name: string
|
||||
itemPlaceholder: string
|
||||
icon?: string
|
||||
list: string[]
|
||||
errors: boolean[]
|
||||
setList: React.Dispatch<React.SetStateAction<string[]>>
|
||||
}
|
||||
|
||||
const FormTextList = ({
|
||||
name,
|
||||
itemPlaceholder,
|
||||
icon,
|
||||
list,
|
||||
errors,
|
||||
setList,
|
||||
}: FormTextListProps) => {
|
||||
return (
|
||||
<div className="flex flex-col space-y-2 bg-base-100 rounded-xl">
|
||||
<div className="flex space-x-2 justify-between">
|
||||
<div className="flex items-center">
|
||||
<p className="font-semibold">{name}</p>
|
||||
</div>
|
||||
<button
|
||||
className="btn btn-square btn-sm"
|
||||
type="button"
|
||||
onClick={() => setList([...list, ''])}
|
||||
>
|
||||
<Icon path={mdiPlus} className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
{list.map((item, i) => (
|
||||
<label
|
||||
className={`input input-bordered pr-2 flex items-center space-x-2 w-full ${errors[i] && 'input-error'}`}
|
||||
key={i}
|
||||
>
|
||||
<label
|
||||
className={`size-4 flex-none ${errors[i] ? 'text-error' : 'opacity-70'}`}
|
||||
>
|
||||
<Icon path={icon} />
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder={itemPlaceholder}
|
||||
className="flex-grow min-w-0"
|
||||
value={item}
|
||||
onChange={(e) =>
|
||||
setList(list.map((val, j) => (j === i ? e.target.value : val)))
|
||||
}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-square btn-sm flex-none"
|
||||
type="button"
|
||||
onClick={() => setList(list.filter((_, j) => j !== i))}
|
||||
>
|
||||
<Icon path={mdiDelete} className="size-4 text-error" />
|
||||
</button>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FormTextList
|
@ -45,7 +45,6 @@ const UPDATE_PROJECT_MUTATION: TypedDocumentNode<
|
||||
`
|
||||
|
||||
export const Loading = () => <CellLoading />
|
||||
|
||||
export const Failure = ({ error }: CellFailureProps) => (
|
||||
<CellFailure error={error} />
|
||||
)
|
||||
@ -58,33 +57,37 @@ export const Success = ({ project }: CellSuccessProps<EditProjectById>) => {
|
||||
toast.success('Project updated')
|
||||
navigate(routes.projects())
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
onError: (error) => toast.error(error.message),
|
||||
}
|
||||
)
|
||||
|
||||
const onSave = (
|
||||
input: UpdateProjectInput,
|
||||
id: EditProjectById['project']['id']
|
||||
) => {
|
||||
updateProject({ variables: { id, input } })
|
||||
}
|
||||
) => updateProject({ variables: { id, input } })
|
||||
|
||||
return (
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">
|
||||
Edit Project {project?.id}
|
||||
</h2>
|
||||
</header>
|
||||
<div className="rw-segment-main">
|
||||
<ProjectForm
|
||||
project={project}
|
||||
onSave={onSave}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
<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">Edit Project {project.id}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
<ProjectForm
|
||||
project={project}
|
||||
onSave={onSave}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -30,23 +30,30 @@ const NewProject = () => {
|
||||
toast.success('Project created')
|
||||
navigate(routes.projects())
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
onError: (error) => toast.error(error.message),
|
||||
}
|
||||
)
|
||||
|
||||
const onSave = (input: CreateProjectInput) => {
|
||||
const onSave = (input: CreateProjectInput) =>
|
||||
createProject({ variables: { input } })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">New Project</h2>
|
||||
</header>
|
||||
<div className="rw-segment-main">
|
||||
<ProjectForm onSave={onSave} loading={loading} error={error} />
|
||||
<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>New Project</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>
|
||||
<ProjectForm onSave={onSave} error={error} loading={loading} />
|
||||
</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
@ -32,66 +32,76 @@ const Project = ({ project }: Props) => {
|
||||
toast.success('Project deleted')
|
||||
navigate(routes.projects())
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
onError: (error) => toast.error(error.message),
|
||||
})
|
||||
|
||||
const onDeleteClick = (id: DeleteProjectMutationVariables['id']) => {
|
||||
if (confirm('Are you sure you want to delete project ' + id + '?')) {
|
||||
if (confirm('Are you sure you want to delete project ' + id + '?'))
|
||||
deleteProject({ variables: { id } })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rw-segment">
|
||||
<header className="rw-segment-header">
|
||||
<h2 className="rw-heading rw-heading-secondary">
|
||||
Project {project.id} Detail
|
||||
</h2>
|
||||
</header>
|
||||
<table className="rw-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
<td>{project.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<td>{project.title}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<td>{project.description}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<td>{timeTag(project.date)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Links</th>
|
||||
<td>{project.links}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="flex w-full justify-center">
|
||||
<div>
|
||||
<div className="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">
|
||||
Project {project.id}: {project.title}
|
||||
</th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<td>{project.id}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Title</th>
|
||||
<td>{project.title}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Description</th>
|
||||
<td>{project.description}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<td>{timeTag(project.date)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Links</th>
|
||||
<td className="space-x-2 space-y-2">
|
||||
{project.links.map((link, i) => (
|
||||
<div className="badge badge-ghost text-nowrap" key={i}>
|
||||
{link}
|
||||
</div>
|
||||
))}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<nav className="my-2 flex justify-center space-x-2">
|
||||
<Link
|
||||
to={routes.editProject({ id: project.id })}
|
||||
title={'Edit project ' + project.id}
|
||||
className="btn btn-primary btn-sm uppercase"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
title={'Delete project ' + project.id}
|
||||
className="btn btn-error btn-sm uppercase"
|
||||
onClick={() => onDeleteClick(project.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
<nav className="rw-button-group">
|
||||
<Link
|
||||
to={routes.editProject({ id: project.id })}
|
||||
className="rw-button rw-button-blue"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
className="rw-button rw-button-red"
|
||||
onClick={() => onDeleteClick(project.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</nav>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -27,13 +27,10 @@ export const QUERY: TypedDocumentNode<
|
||||
`
|
||||
|
||||
export const Loading = () => <CellLoading />
|
||||
|
||||
export const Empty = () => <CellEmpty />
|
||||
|
||||
export const Failure = ({
|
||||
error,
|
||||
}: CellFailureProps<FindProjectByIdVariables>) => <CellFailure error={error} />
|
||||
|
||||
export const Success = ({
|
||||
project,
|
||||
}: CellSuccessProps<FindProjectById, FindProjectByIdVariables>) => (
|
||||
|
@ -1,17 +1,26 @@
|
||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { mdiFormatTitle, mdiLinkVariant } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
import type { EditProjectById, UpdateProjectInput } from 'types/graphql'
|
||||
|
||||
import type { RWGqlError } from '@redwoodjs/forms'
|
||||
import {
|
||||
Form,
|
||||
FormError,
|
||||
FieldError,
|
||||
Label,
|
||||
TextField,
|
||||
Submit,
|
||||
TextAreaField,
|
||||
} from '@redwoodjs/forms'
|
||||
import { toast } from '@redwoodjs/web/toast'
|
||||
|
||||
import FormTextList from 'src/components/FormTextList/FormTextList'
|
||||
|
||||
type FormProject = NonNullable<EditProjectById['project']>
|
||||
|
||||
// TODO: add project images
|
||||
|
||||
interface ProjectFormProps {
|
||||
project?: EditProjectById['project']
|
||||
onSave: (data: UpdateProjectInput, id?: FormProject['id']) => void
|
||||
@ -20,81 +29,110 @@ interface ProjectFormProps {
|
||||
}
|
||||
|
||||
const ProjectForm = (props: ProjectFormProps) => {
|
||||
const [links, setLinks] = useState<string[]>(props.project?.links || [])
|
||||
const [linkErrors, setLinkErrors] = useState<boolean[]>([])
|
||||
|
||||
const urlRegex = useMemo(
|
||||
() =>
|
||||
/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)(?:\.(?:[a-z\u00a1-\uffff0-9]-*)*[a-z\u00a1-\uffff0-9]+)*(?:\.(?:[a-z\u00a1-\uffff]{2,})))(?::\d{2,5})?(?:[/?#]\S*)?$/i,
|
||||
[]
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
setLinkErrors(links.map((link) => link.length > 0 && !urlRegex.test(link)))
|
||||
}, [links, urlRegex])
|
||||
|
||||
const onSubmit = (data: FormProject) => {
|
||||
const errorsExist = linkErrors.indexOf(true) !== -1
|
||||
const errorCount = linkErrors.filter((val) => val).length
|
||||
const emptyCount = links.filter((val) => val.trim().length === 0).length
|
||||
|
||||
if (errorsExist) return toast.error(`${errorCount} links invalid`)
|
||||
if (emptyCount > 0) return toast.error(`${emptyCount} links empty`)
|
||||
|
||||
data.links = links
|
||||
data.date = new Date().toISOString() // TODO: change to date picker value
|
||||
props.onSave(data, props?.project?.id)
|
||||
}
|
||||
|
||||
const titleRef = useRef<HTMLInputElement>(null)
|
||||
useEffect(() => titleRef.current?.focus(), [])
|
||||
|
||||
return (
|
||||
<div className="rw-form-wrapper">
|
||||
<Form<FormProject> onSubmit={onSubmit} error={props.error}>
|
||||
<FormError
|
||||
error={props.error}
|
||||
wrapperClassName="rw-form-error-wrapper"
|
||||
titleClassName="rw-form-error-title"
|
||||
listClassName="rw-form-error-list"
|
||||
/>
|
||||
|
||||
<Form<FormProject>
|
||||
onSubmit={onSubmit}
|
||||
error={props.error}
|
||||
className="space-y-2 w-80"
|
||||
>
|
||||
<Label name="title" className="form-control w-full">
|
||||
<Label
|
||||
name="title"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
className="input input-bordered flex items-center gap-2"
|
||||
errorClassName="input input-bordered flex items-center gap-2 input-error"
|
||||
>
|
||||
Title
|
||||
<Label
|
||||
name="title"
|
||||
className="size-4 opacity-70"
|
||||
errorClassName="size-4 text-error"
|
||||
>
|
||||
<Icon path={mdiFormatTitle} />
|
||||
</Label>
|
||||
<TextField
|
||||
name="title"
|
||||
ref={titleRef}
|
||||
placeholder="Title"
|
||||
defaultValue={props.project?.title}
|
||||
className="w-full"
|
||||
validation={{
|
||||
required: {
|
||||
value: true,
|
||||
message: 'Required',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</Label>
|
||||
<div className="label">
|
||||
<FieldError
|
||||
name="title"
|
||||
className="text-xs font-semibold text-error"
|
||||
/>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<TextField
|
||||
name="title"
|
||||
defaultValue={props.project?.title}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
|
||||
<FieldError name="title" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="description"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Description
|
||||
</Label>
|
||||
|
||||
<TextField
|
||||
<Label name="description" className="form-control w-full">
|
||||
<TextAreaField
|
||||
name="description"
|
||||
defaultValue={props.project?.description}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
className="textarea textarea-bordered"
|
||||
errorClassName="textarea textarea-bordered textarea-error"
|
||||
placeholder="Description"
|
||||
/>
|
||||
|
||||
<FieldError name="description" className="rw-field-error" />
|
||||
|
||||
<Label
|
||||
name="links"
|
||||
className="rw-label"
|
||||
errorClassName="rw-label rw-label-error"
|
||||
>
|
||||
Links
|
||||
</Label>
|
||||
|
||||
<TextField
|
||||
name="links"
|
||||
defaultValue={props.project?.links}
|
||||
className="rw-input"
|
||||
errorClassName="rw-input rw-input-error"
|
||||
validation={{ required: true }}
|
||||
/>
|
||||
|
||||
<FieldError name="links" className="rw-field-error" />
|
||||
|
||||
<div className="rw-button-group">
|
||||
<Submit disabled={props.loading} className="rw-button rw-button-blue">
|
||||
Save
|
||||
</Submit>
|
||||
<div className="label">
|
||||
<FieldError
|
||||
name="description"
|
||||
className="text-xs font-semibold text-error"
|
||||
/>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Label>
|
||||
|
||||
<FormTextList
|
||||
name="Links"
|
||||
itemPlaceholder="URL"
|
||||
icon={mdiLinkVariant}
|
||||
list={links}
|
||||
errors={linkErrors}
|
||||
setList={setLinks}
|
||||
/>
|
||||
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { mdiDotsVertical } from '@mdi/js'
|
||||
import Icon from '@mdi/react'
|
||||
import type {
|
||||
DeleteProjectMutation,
|
||||
DeleteProjectMutationVariables,
|
||||
@ -25,74 +27,97 @@ const DELETE_PROJECT_MUTATION: TypedDocumentNode<
|
||||
|
||||
const ProjectsList = ({ projects }: FindProjects) => {
|
||||
const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION, {
|
||||
onCompleted: () => {
|
||||
toast.success('Project deleted')
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
// This refetches the query on the list page. Read more about other ways to
|
||||
// update the cache over here:
|
||||
// https://www.apollographql.com/docs/react/data/mutations/#making-all-other-cache-updates
|
||||
onCompleted: () => toast.success('Project deleted'),
|
||||
onError: (error) => toast.error(error.message),
|
||||
refetchQueries: [{ query: QUERY }],
|
||||
awaitRefetchQueries: true,
|
||||
})
|
||||
|
||||
const onDeleteClick = (id: DeleteProjectMutationVariables['id']) => {
|
||||
if (confirm('Are you sure you want to delete project ' + id + '?')) {
|
||||
if (confirm('Are you sure you want to delete project ' + id + '?'))
|
||||
deleteProject({ variables: { id } })
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="rw-segment rw-table-wrapper-responsive">
|
||||
<table className="rw-table">
|
||||
<thead>
|
||||
<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>Id</th>
|
||||
<th>Title</th>
|
||||
<th>Description</th>
|
||||
<th>Date</th>
|
||||
<th>Links</th>
|
||||
<th> </th>
|
||||
<th className="w-0"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{projects.map((project) => (
|
||||
<tr key={project.id}>
|
||||
<td>{truncate(project.id)}</td>
|
||||
<td>{truncate(project.title)}</td>
|
||||
<td>{truncate(project.description)}</td>
|
||||
<td>{timeTag(project.date)}</td>
|
||||
<td>{truncate(project.links)}</td>
|
||||
<td>
|
||||
<nav className="rw-table-actions">
|
||||
<Link
|
||||
to={routes.project({ id: project.id })}
|
||||
title={'Show project ' + project.id + ' detail'}
|
||||
className="rw-button rw-button-small"
|
||||
>
|
||||
Show
|
||||
</Link>
|
||||
<Link
|
||||
to={routes.editProject({ id: project.id })}
|
||||
title={'Edit project ' + project.id}
|
||||
className="rw-button rw-button-small rw-button-blue"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
title={'Delete project ' + project.id}
|
||||
className="rw-button rw-button-small rw-button-red"
|
||||
onClick={() => onDeleteClick(project.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</nav>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{projects.map((project) => {
|
||||
const actionButtons = (
|
||||
<>
|
||||
<Link
|
||||
to={routes.project({ id: project.id })}
|
||||
title={'Show project ' + project.id + ' detail'}
|
||||
className="btn btn-xs uppercase"
|
||||
>
|
||||
Show
|
||||
</Link>
|
||||
<Link
|
||||
to={routes.editProject({ id: project.id })}
|
||||
title={'Edit project ' + project.id}
|
||||
className="btn btn-primary btn-xs uppercase"
|
||||
>
|
||||
Edit
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
title={'Delete projectt ' + project.id}
|
||||
className="btn btn-error btn-xs uppercase"
|
||||
onClick={() => onDeleteClick(project.id)}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
</>
|
||||
)
|
||||
|
||||
return (
|
||||
<tr key={project.id}>
|
||||
<td>{truncate(project.title)}</td>
|
||||
<td>{truncate(project.description)}</td>
|
||||
<td>{timeTag(project.date)}</td>
|
||||
<td className="space-x-2 space-y-2">
|
||||
{project.links.map((link, i) => (
|
||||
<div className="badge badge-ghost text-nowrap" key={i}>
|
||||
{link}
|
||||
</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>
|
||||
|
@ -25,13 +25,10 @@ export const QUERY: TypedDocumentNode<FindProjects, FindProjectsVariables> =
|
||||
`
|
||||
|
||||
export const Loading = () => <CellLoading />
|
||||
|
||||
export const Empty = () => <CellEmpty />
|
||||
|
||||
export const Failure = ({ error }: CellFailureProps<FindProjects>) => (
|
||||
<CellFailure error={error} />
|
||||
)
|
||||
|
||||
export const Success = ({
|
||||
projects,
|
||||
}: CellSuccessProps<FindProjects, FindProjectsVariables>) => (
|
||||
|
@ -44,9 +44,7 @@ const UPDATE_SOCIAL_MUTATION: TypedDocumentNode<
|
||||
`
|
||||
|
||||
export const Loading = () => <CellLoading />
|
||||
|
||||
export const Empty = () => <CellEmpty />
|
||||
|
||||
export const Failure = ({ error }: CellFailureProps) => (
|
||||
<CellFailure error={error} />
|
||||
)
|
||||
@ -77,17 +75,21 @@ export const Success = ({ social }: CellSuccessProps<EditSocialById>) => {
|
||||
<div className="overflow-hidden overflow-x-auto rounded-xl bg-base-100">
|
||||
<table className="table w-80">
|
||||
<thead className="bg-base-200 font-syne">
|
||||
<th className="w-0">Edit Social {social.id}</th>
|
||||
<tr>
|
||||
<th className="w-0">Edit Social {social.id}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<th>
|
||||
<SocialForm
|
||||
social={social}
|
||||
onSave={onSave}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
</th>
|
||||
<tr>
|
||||
<th>
|
||||
<SocialForm
|
||||
social={social}
|
||||
onSave={onSave}
|
||||
error={error}
|
||||
loading={loading}
|
||||
/>
|
||||
</th>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
@ -32,9 +32,7 @@ const Social = ({ social }: Props) => {
|
||||
toast.success('Social deleted')
|
||||
navigate(routes.socials())
|
||||
},
|
||||
onError: (error) => {
|
||||
toast.error(error.message)
|
||||
},
|
||||
onError: (error) => toast.error(error.message),
|
||||
})
|
||||
|
||||
const onDeleteClick = (
|
||||
|
@ -86,8 +86,8 @@ const SocialForm = (props: SocialFormProps) => {
|
||||
>
|
||||
<Label
|
||||
name="name"
|
||||
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={mdiRename} />
|
||||
</Label>
|
||||
|
@ -45,8 +45,8 @@ const TagForm = (props: TagFormProps) => {
|
||||
>
|
||||
<Label
|
||||
name="tag"
|
||||
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={mdiTag} />
|
||||
</Label>
|
||||
|
@ -51,8 +51,8 @@ const ForgotPasswordPage = () => {
|
||||
>
|
||||
<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>
|
||||
|
@ -75,8 +75,8 @@ const ResetPasswordPage = ({ resetToken }: { resetToken: string }) => {
|
||||
>
|
||||
<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>
|
||||
|
Reference in New Issue
Block a user