Create project schema and scaffold + some minor changes
This commit is contained in:
49
api/db/migrations/20240824001030_project/migration.sql
Normal file
49
api/db/migrations/20240824001030_project/migration.sql
Normal 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;
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
"globals": {
|
||||
"context": {
|
||||
"writable": false
|
||||
},
|
||||
"gql": {
|
||||
"writable": false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
@@ -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')
|
||||
})
|
||||
})
|
||||
@@ -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()
|
||||
|
||||
@@ -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 {
|
||||
33
api/src/graphql/projectImages.sdl.ts
Normal file
33
api/src/graphql/projectImages.sdl.ts
Normal 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
|
||||
}
|
||||
`
|
||||
36
api/src/graphql/projects.sdl.ts
Normal file
36
api/src/graphql/projects.sdl.ts
Normal 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
|
||||
}
|
||||
`
|
||||
3
api/src/graphql/scalars.sdl.ts
Normal file
3
api/src/graphql/scalars.sdl.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export const schema = gql`
|
||||
scalar URL
|
||||
`
|
||||
29
api/src/graphql/tags.sdl.ts
Normal file
29
api/src/graphql/tags.sdl.ts
Normal 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
|
||||
}
|
||||
`
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`,
|
||||
}
|
||||
}
|
||||
|
||||
49
api/src/services/projectImages/projectImages.ts
Normal file
49
api/src/services/projectImages/projectImages.ts
Normal 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()
|
||||
},
|
||||
}
|
||||
50
api/src/services/projects/projects.ts
Normal file
50
api/src/services/projects/projects.ts
Normal 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()
|
||||
},
|
||||
}
|
||||
42
api/src/services/tags/tags.ts
Normal file
42
api/src/services/tags/tags.ts
Normal 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()
|
||||
},
|
||||
}
|
||||
@@ -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}>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ export const QUERY: TypedDocumentNode<
|
||||
FindPortrait,
|
||||
FindPortraitVariables
|
||||
> = gql`
|
||||
query FindPortrait {
|
||||
query ContactCardPortrait {
|
||||
portrait: portrait {
|
||||
fileId
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
55
web/src/components/Project/NewProject/NewProject.tsx
Normal file
55
web/src/components/Project/NewProject/NewProject.tsx
Normal 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
|
||||
98
web/src/components/Project/Project/Project.tsx
Normal file
98
web/src/components/Project/Project/Project.tsx
Normal 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
|
||||
40
web/src/components/Project/ProjectCell/ProjectCell.tsx
Normal file
40
web/src/components/Project/ProjectCell/ProjectCell.tsx
Normal 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} />
|
||||
}
|
||||
101
web/src/components/Project/ProjectForm/ProjectForm.tsx
Normal file
101
web/src/components/Project/ProjectForm/ProjectForm.tsx
Normal 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
|
||||
102
web/src/components/Project/Projects/Projects.tsx
Normal file
102
web/src/components/Project/Projects/Projects.tsx
Normal 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> </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
|
||||
48
web/src/components/Project/ProjectsCell/ProjectsCell.tsx
Normal file
48
web/src/components/Project/ProjectsCell/ProjectsCell.tsx
Normal 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} />
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
11
web/src/pages/Project/EditProjectPage/EditProjectPage.tsx
Normal file
11
web/src/pages/Project/EditProjectPage/EditProjectPage.tsx
Normal 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
|
||||
7
web/src/pages/Project/NewProjectPage/NewProjectPage.tsx
Normal file
7
web/src/pages/Project/NewProjectPage/NewProjectPage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import NewProject from 'src/components/Project/NewProject'
|
||||
|
||||
const NewProjectPage = () => {
|
||||
return <NewProject />
|
||||
}
|
||||
|
||||
export default NewProjectPage
|
||||
11
web/src/pages/Project/ProjectPage/ProjectPage.tsx
Normal file
11
web/src/pages/Project/ProjectPage/ProjectPage.tsx
Normal 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
|
||||
7
web/src/pages/Project/ProjectsPage/ProjectsPage.tsx
Normal file
7
web/src/pages/Project/ProjectsPage/ProjectsPage.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import ProjectsCell from 'src/components/Project/ProjectsCell'
|
||||
|
||||
const ProjectsPage = () => {
|
||||
return <ProjectsCell />
|
||||
}
|
||||
|
||||
export default ProjectsPage
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user