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

@@ -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