Public facing projects showcase, individual project details not done yet

This commit is contained in:
Ahmed Al-Taiar
2024-09-29 15:38:26 -04:00
parent 38168db452
commit 9c0dee7d54
20 changed files with 287 additions and 38 deletions

View File

@@ -30,8 +30,8 @@ const Routes = () => {
<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" />
<Route path="/admin/projects/{id:Int}" page={ProjectProjectPage} name="adminProject" />
<Route path="/admin/projects" page={ProjectProjectsPage} name="adminProjects" />
</Set>
</PrivateSet>
@@ -49,6 +49,8 @@ const Routes = () => {
<Set wrap={NavbarLayout}>
<Route path="/" page={HomePage} name="home" />
<Route path="/projects" page={ProjectsPage} name="projects" />
<Route path="/project/{id:Int}" page={ProjectPage} name="project" />
<Route path="/contact" page={ContactPage} name="contact" />
</Set>

View File

@@ -0,0 +1,56 @@
import { useState, useRef, useCallback, useEffect } from 'react'
const SCROLL_INTERVAL_SECONDS = 3
interface AutoCarouselProps {
images: string[]
}
const AutoCarousel = ({ images }: AutoCarouselProps) => {
const [activeItem, setActiveItem] = useState<number>(0)
const ref = useRef<HTMLDivElement>(null)
const scroll = useCallback(() => {
setActiveItem((prev) => {
if (images.length - 1 > prev) return prev + 1
else return 0
})
}, [images.length])
const autoScroll = useCallback(
() => setInterval(scroll, SCROLL_INTERVAL_SECONDS * 1000),
[scroll]
)
useEffect(() => {
const play = autoScroll()
return () => clearInterval(play)
}, [autoScroll])
useEffect(() => {
const width = ref.current?.getBoundingClientRect().width
ref.current?.scroll({ left: activeItem * (width || 0) })
}, [activeItem])
return (
<div
ref={ref}
className="carousel carousel-center p-2 space-x-2 rounded-box"
>
{images.map((image, i) => (
<div
key={i}
className="carousel-item w-full h-fit my-auto justify-center"
>
<img
src={image}
alt={`${i}`}
className="object-contain rounded-xl size-fit"
/>
</div>
))}
</div>
)
}
export default AutoCarousel

View File

@@ -61,7 +61,7 @@ export const Success = ({ project }: CellSuccessProps<EditProjectById>) => {
{
onCompleted: () => {
toast.success('Project updated')
navigate(routes.projects())
navigate(routes.adminProjects())
},
onError: (error) => toast.error(error.message),
}

View File

@@ -28,7 +28,7 @@ const NewProject = () => {
{
onCompleted: () => {
toast.success('Project created')
navigate(routes.projects())
navigate(routes.adminProjects())
},
onError: (error) => toast.error(error.message),
}

View File

@@ -32,7 +32,7 @@ const Project = ({ project }: Props) => {
const [deleteProject] = useMutation(DELETE_PROJECT_MUTATION, {
onCompleted: () => {
toast.success('Project deleted')
navigate(routes.projects())
navigate(routes.adminProjects())
},
onError: (error) => toast.error(error.message),
})
@@ -99,7 +99,7 @@ const Project = ({ project }: Props) => {
{project.tags.map((tag, i) => (
<div
key={i}
className="badge"
className="badge whitespace-nowrap"
style={{
backgroundColor: tag.color,
color:

View File

@@ -235,30 +235,33 @@ const ProjectForm = (props: ProjectFormProps) => {
</div>
{fileIds.length > 0 ? (
<>
{fileIds.map((fileId, i) => (
<div
key={i}
className="card rounded-xl image-full image-full-no-overlay"
>
<figure>
<img src={fileId} alt={i.toString()} />
</figure>
<div className="card-body p-2 rounded-xl">
<div className="card-actions rounded-md justify-end">
<button
type="button"
className="btn btn-square btn-sm shadow-xl"
onClick={() => {
setToDelete([...toDelete, fileId])
setFileIds(fileIds.filter((id) => id !== fileId))
}}
>
<Icon path={mdiDelete} className="size-4 text-error" />
</button>
{!appendUploader &&
fileIds.map((fileId, i) => (
<div key={i} className="flex justify-center">
<div className="card rounded-xl w-fit image-full image-full-no-overlay">
<figure>
<img src={fileId} alt={i.toString()} />
</figure>
<div className="card-body p-2 rounded-xl">
<div className="card-actions rounded-md justify-end">
<button
type="button"
className="btn btn-square btn-sm shadow-xl"
onClick={() => {
setToDelete([...toDelete, fileId])
setFileIds(fileIds.filter((id) => id !== fileId))
}}
>
<Icon
path={mdiDelete}
className="size-4 text-error"
/>
</button>
</div>
</div>
</div>
</div>
</div>
))}
))}
{appendUploader && (
<Uploader
onComplete={onUploadComplete}

View File

@@ -61,7 +61,7 @@ const ProjectsList = ({ projects }: FindProjects) => {
const actionButtons = (
<>
<Link
to={routes.project({ id: project.id })}
to={routes.adminProject({ id: project.id })}
title={'Show project ' + project.id + ' detail'}
className="btn btn-xs uppercase"
>
@@ -96,7 +96,7 @@ const ProjectsList = ({ projects }: FindProjects) => {
{project.tags.map((tag, i) => (
<div
key={i}
className="badge"
className="badge whitespace-nowrap"
style={{
backgroundColor: tag.color,
color:

View File

@@ -0,0 +1,59 @@
import { format, isAfter, startOfToday } from 'date-fns'
import { FindProjects } from 'types/graphql'
import { Link, routes } from '@redwoodjs/router'
import AutoCarousel from 'src/components/AutoCarousel/AutoCarousel'
import { calculateLuminance } from 'src/lib/color'
const ProjectsShowcase = ({ projects }: FindProjects) => (
<div className="flex flex-wrap justify-center gap-2">
{projects
.slice()
.sort((a, b) => (isAfter(new Date(b.date), new Date(a.date)) ? 1 : -1))
.map((project, i) => (
<Link key={i} to={routes.project({ id: project.id })}>
<div className="card card-compact bg-base-100 w-96 h-fit shadow-xl transition-all hover:-translate-y-2 hover:shadow-2xl">
{project.images.length > 0 && (
<AutoCarousel images={project.images} />
)}
<div className="card-body">
<div className="card-title overflow-auto">
<p className="whitespace-nowrap">{project.title}</p>
</div>
<div className="line-clamp-5">{project.description}</div>
<div className="card-actions justify-between">
<div className="flex gap-2">
{isAfter(new Date(project.date), startOfToday()) && (
<div className="badge badge-info">planned</div>
)}
<div className="badge badge-ghost">
{format(project.date, 'yyyy-MM-dd')}
</div>
</div>
<div className="flex flex-wrap gap-2">
{project.tags.map((tag, i) => (
<div
key={i}
className="badge whitespace-nowrap"
style={{
backgroundColor: tag.color,
color:
calculateLuminance(tag.color) > 0.5
? 'black'
: 'white',
}}
>
{tag.tag}
</div>
))}
</div>
</div>
</div>
</div>
</Link>
))}
</div>
)
export default ProjectsShowcase

View File

@@ -0,0 +1,44 @@
import type { FindProjects, FindProjectsVariables } from 'types/graphql'
import type {
CellSuccessProps,
CellFailureProps,
TypedDocumentNode,
} from '@redwoodjs/web'
import CellEmpty from 'src/components/Cell/CellEmpty/CellEmpty'
import CellFailure from 'src/components/Cell/CellFailure/CellFailure'
import CellLoading from 'src/components/Cell/CellLoading/CellLoading'
import ProjectsShowcase from '../ProjectsShowcase/ProjectsShowcase'
export const QUERY: TypedDocumentNode<FindProjects, FindProjectsVariables> =
gql`
query FindProjects {
projects {
id
title
description
images
date
links
tags {
id
tag
color
}
}
}
`
export const Loading = () => <CellLoading />
export const Empty = () => <CellEmpty />
export const Failure = ({ error }: CellFailureProps<FindProjectsVariables>) => (
<CellFailure error={error} />
)
export const Success = ({
projects,
}: CellSuccessProps<FindProjects, FindProjectsVariables>) => (
<ProjectsShowcase projects={projects} />
)

View File

@@ -65,7 +65,7 @@ const Tag = ({ tag }: Props) => {
<th>Color</th>
<td>
<div
className="badge"
className="badge whitespace-nowrap"
style={{
backgroundColor: tag.color,
color:

View File

@@ -96,7 +96,7 @@ const TagsList = ({ tags }: FindTags) => {
<td>{truncate(tag.tag)}</td>
<td>
<div
className="badge"
className="badge whitespace-nowrap"
style={{
backgroundColor: tag.color,
color:

View File

@@ -20,6 +20,10 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
const { isAuthenticated, logOut } = useAuth()
const navbarRoutes: NavbarRoute[] = [
{
name: 'Projects',
path: routes.projects(),
},
{
name: 'Contact',
path: routes.contact(),
@@ -33,7 +37,7 @@ const NavbarLayout = ({ children }: NavbarLayoutProps) => {
},
{
name: 'Projects',
path: routes.projects(),
path: routes.adminProjects(),
},
{
name: 'Tags',

View File

@@ -0,0 +1,27 @@
import { Metadata } from '@redwoodjs/web'
interface ProjectPageProps {
id: number
}
// TODO: implement
const ProjectPage = ({ id }: ProjectPageProps) => {
return (
<>
<Metadata title="Project" />
<h1>ProjectPage</h1>
<p>
Find me in <code>./web/src/pages/ProjectPage/ProjectPage.tsx</code>
</p>
<p>My id is: {id}</p>
{/*
My default route is named `project`, link to me with:
`<Link to={routes.project()}>Project</Link>`
*/}
</>
)
}
export default ProjectPage

View File

@@ -0,0 +1,29 @@
import { isMobile, isBrowser } from 'react-device-detect'
import { Metadata } from '@redwoodjs/web'
import ProjectsShowcaseCell from 'src/components/Project/ProjectsShowcaseCell'
const ProjectsPage = () => {
return (
<>
<Metadata title="Projects" />
<div className="hero min-h-48">
<div className="hero-content">
<div className="max-w-md text-center">
<h1 className="text-5xl font-bold">Projects</h1>
<p className="py-6">
{isBrowser && !isMobile ? 'Click' : 'Tap'} on a project for
details
</p>
</div>
</div>
</div>
<ProjectsShowcaseCell />
</>
)
}
export default ProjectsPage