Create project schema and scaffold + some minor changes

This commit is contained in:
Ahmed Al-Taiar
2024-08-23 20:51:35 -04:00
parent bb4b5708e1
commit 3d031ca73c
38 changed files with 952 additions and 64 deletions

View File

@@ -0,0 +1,49 @@
-- CreateTable
CREATE TABLE "Tag" (
"id" SERIAL NOT NULL,
"tag" TEXT NOT NULL,
"color" TEXT NOT NULL,
CONSTRAINT "Tag_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "ProjectImage" (
"id" SERIAL NOT NULL,
"fileId" TEXT NOT NULL,
"projectId" INTEGER,
CONSTRAINT "ProjectImage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "Project" (
"id" SERIAL NOT NULL,
"title" TEXT NOT NULL,
"description" TEXT NOT NULL DEFAULT 'No description provided',
"date" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"links" TEXT[] DEFAULT ARRAY[]::TEXT[],
CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "_ProjectToTag" (
"A" INTEGER NOT NULL,
"B" INTEGER NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "_ProjectToTag_AB_unique" ON "_ProjectToTag"("A", "B");
-- CreateIndex
CREATE INDEX "_ProjectToTag_B_index" ON "_ProjectToTag"("B");
-- AddForeignKey
ALTER TABLE "ProjectImage" ADD CONSTRAINT "ProjectImage_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ProjectToTag" ADD CONSTRAINT "_ProjectToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_ProjectToTag" ADD CONSTRAINT "_ProjectToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -48,3 +48,28 @@ model Portrait {
id Int @id @default(autoincrement())
fileId String
}
model Tag {
id Int @id @default(autoincrement())
tag String
color String
projects Project[]
}
model ProjectImage {
id Int @id @default(autoincrement())
fileId String
Project Project? @relation(fields: [projectId], references: [id])
projectId Int?
}
model Project {
id Int @id @default(autoincrement())
title String
description String @default("No description provided")
images ProjectImage[]
date DateTime @default(now())
links String[] @default([])
tags Tag[]
}

View File

@@ -11,6 +11,7 @@
"@redwoodjs/graphql-server": "7.7.4",
"@tus/file-store": "^1.4.0",
"@tus/server": "^1.7.0",
"graphql-scalars": "^1.23.0",
"nodemailer": "^6.9.14"
},
"devDependencies": {

View File

@@ -2,6 +2,9 @@
"globals": {
"context": {
"writable": false
},
"gql": {
"writable": false
}
}
}

View File

@@ -1,18 +0,0 @@
import { mockRedwoodDirective, getDirectiveName } from '@redwoodjs/testing/api'
import requireAuth from './requireAuth'
describe('requireAuth directive', () => {
it('declares the directive sdl as schema, with the correct name', () => {
expect(requireAuth.schema).toBeTruthy()
expect(getDirectiveName(requireAuth.schema)).toBe('requireAuth')
})
it('requireAuth has stub implementation. Should not throw when current user', () => {
// If you want to set values in context, pass it through e.g.
// mockRedwoodDirective(requireAuth, { context: { currentUser: { id: 1, name: 'Lebron McGretzky' } }})
const mockExecution = mockRedwoodDirective(requireAuth, { context: {} })
expect(mockExecution).not.toThrowError()
})
})

View File

@@ -1,10 +0,0 @@
import { getDirectiveName } from '@redwoodjs/testing/api'
import skipAuth from './skipAuth'
describe('skipAuth directive', () => {
it('declares the directive sdl as schema, with the correct name', () => {
expect(skipAuth.schema).toBeTruthy()
expect(getDirectiveName(skipAuth.schema)).toBe('skipAuth')
})
})

View File

@@ -1,3 +1,5 @@
import { URLTypeDefinition, URLResolver } from 'graphql-scalars'
import { createAuthDecoder } from '@redwoodjs/auth-dbauth-api'
import { createGraphQLHandler } from '@redwoodjs/graphql-server'
@@ -18,6 +20,12 @@ export const handler = createGraphQLHandler({
directives,
sdls,
services,
schemaOptions: {
typeDefs: [URLTypeDefinition],
resolvers: {
URL: URLResolver,
},
},
onException: () => {
// Disconnect from your database with an unhandled exception.
db.$disconnect()

View File

@@ -1,7 +1,7 @@
export const schema = gql`
type Portrait {
id: Int!
fileId: String!
fileId: URL!
}
type Query {
@@ -9,7 +9,7 @@ export const schema = gql`
}
input CreatePortraitInput {
fileId: String!
fileId: URL!
}
type Mutation {

View File

@@ -0,0 +1,33 @@
export const schema = gql`
type ProjectImage {
id: Int!
fileId: URL!
Project: Project
projectId: Int
}
type Query {
projectImages: [ProjectImage!]! @requireAuth
projectImage(id: Int!): ProjectImage @requireAuth
}
input CreateProjectImageInput {
fileId: URL!
projectId: Int
}
input UpdateProjectImageInput {
fileId: URL
projectId: Int
}
type Mutation {
createProjectImage(input: CreateProjectImageInput!): ProjectImage!
@requireAuth
updateProjectImage(
id: Int!
input: UpdateProjectImageInput!
): ProjectImage! @requireAuth
deleteProjectImage(id: Int!): ProjectImage! @requireAuth
}
`

View File

@@ -0,0 +1,36 @@
export const schema = gql`
type Project {
id: Int!
title: String!
description: String!
images: [ProjectImage]!
date: DateTime!
links: [String]!
tags: [Tag]!
}
type Query {
projects: [Project!]! @requireAuth
project(id: Int!): Project @requireAuth
}
input CreateProjectInput {
title: String!
description: String!
date: DateTime!
links: [String]!
}
input UpdateProjectInput {
title: String
description: String
date: DateTime
links: [String]!
}
type Mutation {
createProject(input: CreateProjectInput!): Project! @requireAuth
updateProject(id: Int!, input: UpdateProjectInput!): Project! @requireAuth
deleteProject(id: Int!): Project! @requireAuth
}
`

View File

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

View File

@@ -0,0 +1,29 @@
export const schema = gql`
type Tag {
id: Int!
tag: String!
color: String!
projects: [Project]!
}
type Query {
tags: [Tag!]! @requireAuth
tag(id: Int!): Tag @requireAuth
}
input CreateTagInput {
tag: String!
color: String!
}
input UpdateTagInput {
tag: String
color: String
}
type Mutation {
createTag(input: CreateTagInput!): Tag! @requireAuth
updateTag(id: Int!, input: UpdateTagInput!): Tag! @requireAuth
deleteTag(id: Int!): Tag! @requireAuth
}
`

View File

@@ -6,7 +6,7 @@ import {
ValidationError,
} from '@redwoodjs/graphql-server'
import { db } from './db'
import { db } from 'src/lib/db'
/**
* The name of the cookie that dbAuth sets

View File

@@ -5,7 +5,7 @@ import { PrismaClient } from '@prisma/client'
import { emitLogLevels, handlePrismaLogging } from '@redwoodjs/api/logger'
import { logger } from './logger'
import { logger } from 'src/lib/logger'
/*
* Instance of the Prisma Client

View File

@@ -1,9 +1,14 @@
import type { QueryResolvers, MutationResolvers } from 'types/graphql'
import { isProduction } from '@redwoodjs/api/dist/logger'
import { ValidationError } from '@redwoodjs/graphql-server'
import { db } from 'src/lib/db'
const address = isProduction
? process.env.ADDRESS_PROD
: process.env.ADDRESS_DEV
export const portrait: QueryResolvers['portrait'] = async () => {
const portrait = await db.portrait.findFirst()
@@ -11,7 +16,7 @@ export const portrait: QueryResolvers['portrait'] = async () => {
else
return {
id: -1,
fileId: '/no_portrait.webp',
fileId: `${address}/no_portrait.webp`,
}
}

View File

@@ -0,0 +1,49 @@
import type {
QueryResolvers,
MutationResolvers,
ProjectImageRelationResolvers,
} from 'types/graphql'
import { db } from 'src/lib/db'
export const projectImages: QueryResolvers['projectImages'] = () => {
return db.projectImage.findMany()
}
export const projectImage: QueryResolvers['projectImage'] = ({ id }) => {
return db.projectImage.findUnique({
where: { id },
})
}
export const createProjectImage: MutationResolvers['createProjectImage'] = ({
input,
}) => {
return db.projectImage.create({
data: input,
})
}
export const updateProjectImage: MutationResolvers['updateProjectImage'] = ({
id,
input,
}) => {
return db.projectImage.update({
data: input,
where: { id },
})
}
export const deleteProjectImage: MutationResolvers['deleteProjectImage'] = ({
id,
}) => {
return db.projectImage.delete({
where: { id },
})
}
export const ProjectImage: ProjectImageRelationResolvers = {
Project: (_obj, { root }) => {
return db.projectImage.findUnique({ where: { id: root?.id } }).Project()
},
}

View File

@@ -0,0 +1,50 @@
import type {
QueryResolvers,
MutationResolvers,
ProjectRelationResolvers,
} from 'types/graphql'
import { db } from 'src/lib/db'
export const projects: QueryResolvers['projects'] = () => {
return db.project.findMany()
}
export const project: QueryResolvers['project'] = ({ id }) => {
return db.project.findUnique({
where: { id },
})
}
export const createProject: MutationResolvers['createProject'] = ({
input,
}) => {
return db.project.create({
data: input,
})
}
export const updateProject: MutationResolvers['updateProject'] = ({
id,
input,
}) => {
return db.project.update({
data: input,
where: { id },
})
}
export const deleteProject: MutationResolvers['deleteProject'] = ({ id }) => {
return 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()
},
}

View File

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

View File

@@ -1,13 +1,12 @@
import { FatalErrorBoundary, RedwoodProvider } from '@redwoodjs/web'
import { RedwoodApolloProvider } from '@redwoodjs/web/apollo'
import { AuthProvider, useAuth } from 'src/auth'
import FatalErrorPage from 'src/pages/FatalErrorPage'
import Routes from 'src/Routes'
import './scaffold.css'
import { AuthProvider, useAuth } from './auth'
import './index.css'
import 'src/scaffold.css'
import 'src/index.css'
const App = () => (
<FatalErrorBoundary page={FatalErrorPage}>

View File

@@ -1,24 +1,30 @@
import { Router, Route, Set, PrivateSet } from '@redwoodjs/router'
import { useAuth } from 'src/auth'
import AccountbarLayout from 'src/layouts/AccountbarLayout'
import NavbarLayout from 'src/layouts/NavbarLayout/NavbarLayout'
import ScaffoldLayout from 'src/layouts/ScaffoldLayout'
import { useAuth } from './auth'
import AccountbarLayout from './layouts/AccountbarLayout/AccountbarLayout'
import NavbarLayout from './layouts/NavbarLayout/NavbarLayout'
const Routes = () => {
return (
<Router useAuth={useAuth}>
<PrivateSet unauthenticated="home">
<Set wrap={ScaffoldLayout} title="Socials" titleTo="socials" buttonLabel="New Social" buttonTo="newSocial">
<Route path="/socials/new" page={SocialNewSocialPage} name="newSocial" />
<Route path="/socials/{id:Int}/edit" page={SocialEditSocialPage} name="editSocial" />
<Route path="/socials/{id:Int}" page={SocialSocialPage} name="social" />
<Route path="/socials" page={SocialSocialsPage} name="socials" />
<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}" page={SocialSocialPage} name="social" />
<Route path="/admin/socials" page={SocialSocialsPage} name="socials" />
</Set>
<Set wrap={ScaffoldLayout} title="Portrait" titleTo="portrait">
<Route path="/portrait" page={PortraitPortraitPage} name="portrait" />
<Route path="/admin/portrait" page={PortraitPortraitPage} name="portrait" />
</Set>
<Set wrap={ScaffoldLayout} title="Projects" titleTo="projects" buttonLabel="New Project" buttonTo="newProject">
<Route path="/admin/projects/new" page={ProjectNewProjectPage} name="newProject" />
<Route path="/admin/projects/{id:Int}/edit" page={ProjectEditProjectPage} name="editProject" />
<Route path="/admin/projects/{id:Int}" page={ProjectProjectPage} name="project" />
<Route path="/admin/projects" page={ProjectProjectsPage} name="projects" />
</Set>
</PrivateSet>

View File

@@ -15,7 +15,7 @@ export const QUERY: TypedDocumentNode<
FindPortrait,
FindPortraitVariables
> = gql`
query FindPortrait {
query ContactCardPortrait {
portrait: portrait {
fileId
}

View File

@@ -6,9 +6,9 @@ import type {
CreatePortraitMutationVariables,
DeletePortraitMutation,
DeletePortraitMutationVariables,
EditPortrait,
FindPortrait,
FindPortraitVariables,
Portrait,
} from 'types/graphql'
import { TypedDocumentNode, useMutation } from '@redwoodjs/web'
@@ -17,14 +17,14 @@ import { toast } from '@redwoodjs/web/dist/toast'
import Uploader from 'src/components/Uploader/Uploader'
interface PortraitFormProps {
portrait?: EditPortrait['portrait']
portrait?: Portrait
}
export const QUERY: TypedDocumentNode<
FindPortrait,
FindPortraitVariables
> = gql`
query FindPortrait {
query PortraitForm {
portrait {
id
fileId

View File

@@ -0,0 +1,89 @@
import type {
EditProjectById,
UpdateProjectInput,
UpdateProjectMutationVariables,
} 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 ProjectForm from 'src/components/Project/ProjectForm'
export const QUERY: TypedDocumentNode<EditProjectById> = gql`
query EditProjectById($id: Int!) {
project: project(id: $id) {
id
title
description
date
links
}
}
`
const UPDATE_PROJECT_MUTATION: TypedDocumentNode<
EditProjectById,
UpdateProjectMutationVariables
> = gql`
mutation UpdateProjectMutation($id: Int!, $input: UpdateProjectInput!) {
updateProject(id: $id, input: $input) {
id
title
description
date
links
}
}
`
export const Loading = () => <div>Loading...</div>
export const Failure = ({ error }: CellFailureProps) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({ project }: CellSuccessProps<EditProjectById>) => {
const [updateProject, { loading, error }] = useMutation(
UPDATE_PROJECT_MUTATION,
{
onCompleted: () => {
toast.success('Project updated')
navigate(routes.projects())
},
onError: (error) => {
toast.error(error.message)
},
}
)
const onSave = (
input: UpdateProjectInput,
id: EditProjectById['project']['id']
) => {
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>
</div>
)
}

View File

@@ -0,0 +1,55 @@
import type {
CreateProjectMutation,
CreateProjectInput,
CreateProjectMutationVariables,
} 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 ProjectForm from 'src/components/Project/ProjectForm'
const CREATE_PROJECT_MUTATION: TypedDocumentNode<
CreateProjectMutation,
CreateProjectMutationVariables
> = gql`
mutation CreateProjectMutation($input: CreateProjectInput!) {
createProject(input: $input) {
id
}
}
`
const NewProject = () => {
const [createProject, { loading, error }] = useMutation(
CREATE_PROJECT_MUTATION,
{
onCompleted: () => {
toast.success('Project created')
navigate(routes.projects())
},
onError: (error) => {
toast.error(error.message)
},
}
)
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>
</div>
)
}
export default NewProject

View File

@@ -0,0 +1,98 @@
import type {
DeleteProjectMutation,
DeleteProjectMutationVariables,
FindProjectById,
} 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 { timeTag } from 'src/lib/formatters'
const DELETE_PROJECT_MUTATION: TypedDocumentNode<
DeleteProjectMutation,
DeleteProjectMutationVariables
> = gql`
mutation DeleteProjectMutation($id: Int!) {
deleteProject(id: $id) {
id
}
}
`
interface Props {
project: NonNullable<FindProjectById['project']>
}
const Project = ({ project }: Props) => {
const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION, {
onCompleted: () => {
toast.success('Project deleted')
navigate(routes.projects())
},
onError: (error) => {
toast.error(error.message)
},
})
const onDeleteClick = (id: DeleteProjectMutationVariables['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>
<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>
</>
)
}
export default Project

View File

@@ -0,0 +1,40 @@
import type { FindProjectById, FindProjectByIdVariables } from 'types/graphql'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import Project from 'src/components/Project/Project'
export const QUERY: TypedDocumentNode<
FindProjectById,
FindProjectByIdVariables
> = gql`
query FindProjectById($id: Int!) {
project: project(id: $id) {
id
title
description
date
links
}
}
`
export const Loading = () => <div>Loading...</div>
export const Empty = () => <div>Project not found</div>
export const Failure = ({
error,
}: CellFailureProps<FindProjectByIdVariables>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
project,
}: CellSuccessProps<FindProjectById, FindProjectByIdVariables>) => {
return <Project project={project} />
}

View File

@@ -0,0 +1,101 @@
import type { EditProjectById, UpdateProjectInput } from 'types/graphql'
import type { RWGqlError } from '@redwoodjs/forms'
import {
Form,
FormError,
FieldError,
Label,
TextField,
Submit,
} from '@redwoodjs/forms'
type FormProject = NonNullable<EditProjectById['project']>
interface ProjectFormProps {
project?: EditProjectById['project']
onSave: (data: UpdateProjectInput, id?: FormProject['id']) => void
error: RWGqlError
loading: boolean
}
const ProjectForm = (props: ProjectFormProps) => {
const onSubmit = (data: FormProject) => {
props.onSave(data, props?.project?.id)
}
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"
/>
<Label
name="title"
className="rw-label"
errorClassName="rw-label rw-label-error"
>
Title
</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
name="description"
defaultValue={props.project?.description}
className="rw-input"
errorClassName="rw-input rw-input-error"
validation={{ required: true }}
/>
<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>
</Form>
</div>
)
}
export default ProjectForm

View File

@@ -0,0 +1,102 @@
import type {
DeleteProjectMutation,
DeleteProjectMutationVariables,
FindProjects,
} 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/Project/ProjectsCell'
import { timeTag, truncate } from 'src/lib/formatters'
const DELETE_PROJECT_MUTATION: TypedDocumentNode<
DeleteProjectMutation,
DeleteProjectMutationVariables
> = gql`
mutation DeleteProjectMutation($id: Int!) {
deleteProject(id: $id) {
id
}
}
`
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
refetchQueries: [{ query: QUERY }],
awaitRefetchQueries: true,
})
const onDeleteClick = (id: DeleteProjectMutationVariables['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>
<tr>
<th>Id</th>
<th>Title</th>
<th>Description</th>
<th>Date</th>
<th>Links</th>
<th>&nbsp;</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>
))}
</tbody>
</table>
</div>
)
}
export default ProjectsList

View File

@@ -0,0 +1,48 @@
import type { FindProjects, FindProjectsVariables } from 'types/graphql'
import { Link, routes } from '@redwoodjs/router'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import Projects from 'src/components/Project/Projects'
export const QUERY: TypedDocumentNode<
FindProjects,
FindProjectsVariables
> = gql`
query FindProjects {
projects {
id
title
description
date
links
}
}
`
export const Loading = () => <div>Loading...</div>
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 Failure = ({ error }: CellFailureProps<FindProjects>) => (
<div className="rw-cell-error">{error?.message}</div>
)
export const Success = ({
projects,
}: CellSuccessProps<FindProjects, FindProjectsVariables>) => {
return <Projects projects={projects} />
}

View File

@@ -1,6 +1,13 @@
import { useEffect, useRef, useState } from 'react'
import { mdiMenuUp, mdiMenuDown, mdiAccount, mdiRename } from '@mdi/js'
import {
mdiMenuUp,
mdiMenuDown,
mdiAccount,
mdiRename,
mdiAt,
mdiLinkVariant,
} from '@mdi/js'
import Icon from '@mdi/react'
import type { EditSocialById, UpdateSocialInput } from 'types/graphql'
@@ -110,10 +117,18 @@ const SocialForm = (props: SocialFormProps) => {
>
<Label
name="username"
className="h-4 w-4 opacity-70"
errorClassName="h-4 w-4 text-error"
className="size-5 opacity-70"
errorClassName="size-5 text-error"
>
<Icon path={mdiAccount} />
<Icon
path={
type == 'email'
? mdiAt
: type == 'custom'
? mdiLinkVariant
: mdiAccount
}
/>
</Label>
<TextField
name="username"

View File

@@ -1,6 +1,6 @@
import { Toaster } from '@redwoodjs/web/dist/toast'
import ToastNotification from '../ToastNotification/ToastNotification'
import ToastNotification from 'src/components/ToastNotification'
const ToasterWrapper = () => (
<Toaster

View File

@@ -1,6 +1,6 @@
import { hydrateRoot, createRoot } from 'react-dom/client'
import App from './App'
import App from 'src/App'
/**
* When `#redwood-app` isn't empty then it's very likely that you're using
* prerendering. So React attaches event listeners to the existing markup

View File

@@ -1,16 +1,9 @@
import { Metadata } from '@redwoodjs/web'
import { useAuth } from 'src/auth'
import Uploader from 'src/components/Uploader/Uploader'
const HomePage = () => {
const { isAuthenticated } = useAuth()
return (
<>
<Metadata title="Home" description="Home page" />
{isAuthenticated && <Uploader />}
</>
)
}

View File

@@ -0,0 +1,11 @@
import EditProjectCell from 'src/components/Project/EditProjectCell'
type ProjectPageProps = {
id: number
}
const EditProjectPage = ({ id }: ProjectPageProps) => {
return <EditProjectCell id={id} />
}
export default EditProjectPage

View File

@@ -0,0 +1,7 @@
import NewProject from 'src/components/Project/NewProject'
const NewProjectPage = () => {
return <NewProject />
}
export default NewProjectPage

View File

@@ -0,0 +1,11 @@
import ProjectCell from 'src/components/Project/ProjectCell'
type ProjectPageProps = {
id: number
}
const ProjectPage = ({ id }: ProjectPageProps) => {
return <ProjectCell id={id} />
}
export default ProjectPage

View File

@@ -0,0 +1,7 @@
import ProjectsCell from 'src/components/Project/ProjectsCell'
const ProjectsPage = () => {
return <ProjectsCell />
}
export default ProjectsPage

View File

@@ -7280,6 +7280,7 @@ __metadata:
"@tus/file-store": "npm:^1.4.0"
"@tus/server": "npm:^1.7.0"
"@types/nodemailer": "npm:^6.4.15"
graphql-scalars: "npm:^1.23.0"
nodemailer: "npm:^6.9.14"
languageName: unknown
linkType: soft
@@ -12683,7 +12684,7 @@ __metadata:
languageName: node
linkType: hard
"graphql-scalars@npm:1.23.0":
"graphql-scalars@npm:1.23.0, graphql-scalars@npm:^1.23.0":
version: 1.23.0
resolution: "graphql-scalars@npm:1.23.0"
dependencies: