diff --git a/api/db/migrations/20240929164343_/migration.sql b/api/db/migrations/20240929164343_/migration.sql new file mode 100644 index 0000000..eb02282 --- /dev/null +++ b/api/db/migrations/20240929164343_/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Project" ALTER COLUMN "images" SET DEFAULT ARRAY[]::TEXT[]; diff --git a/api/db/schema.prisma b/api/db/schema.prisma index a5f8d29..31a5f10 100644 --- a/api/db/schema.prisma +++ b/api/db/schema.prisma @@ -69,7 +69,7 @@ model Project { id Int @id @default(autoincrement()) title String description String @default("No description provided") - images String[] + images String[] @default([]) date DateTime links String[] @default([]) tags Tag[] diff --git a/api/src/graphql/projects.sdl.ts b/api/src/graphql/projects.sdl.ts index f211566..ca2c304 100644 --- a/api/src/graphql/projects.sdl.ts +++ b/api/src/graphql/projects.sdl.ts @@ -10,8 +10,8 @@ export const schema = gql` } type Query { - projects: [Project!]! @requireAuth - project(id: Int!): Project @requireAuth + projects: [Project!]! @skipAuth + project(id: Int!): Project @skipAuth } input CreateProjectInput { diff --git a/api/src/graphql/tags.sdl.ts b/api/src/graphql/tags.sdl.ts index f4f11fb..4d48816 100644 --- a/api/src/graphql/tags.sdl.ts +++ b/api/src/graphql/tags.sdl.ts @@ -7,8 +7,8 @@ export const schema = gql` } type Query { - tags: [Tag!]! @requireAuth - tag(id: Int!): Tag @requireAuth + tags: [Tag!]! @skipAuth + tag(id: Int!): Tag @skipAuth } input CreateTagInput { diff --git a/web/package.json b/web/package.json index 19bfc0f..ecc2965 100644 --- a/web/package.json +++ b/web/package.json @@ -31,6 +31,7 @@ "humanize-string": "2.1.0", "react": "18.3.1", "react-colorful": "^5.6.1", + "react-device-detect": "^2.2.3", "react-dom": "18.3.1" }, "devDependencies": { diff --git a/web/src/Routes.tsx b/web/src/Routes.tsx index dd2e0ac..5ac5725 100644 --- a/web/src/Routes.tsx +++ b/web/src/Routes.tsx @@ -30,8 +30,8 @@ const Routes = () => { - - + + @@ -49,6 +49,8 @@ const Routes = () => { + + diff --git a/web/src/components/AutoCarousel/AutoCarousel.tsx b/web/src/components/AutoCarousel/AutoCarousel.tsx new file mode 100644 index 0000000..14ec8ab --- /dev/null +++ b/web/src/components/AutoCarousel/AutoCarousel.tsx @@ -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(0) + const ref = useRef(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 ( +
+ {images.map((image, i) => ( +
+ {`${i}`} +
+ ))} +
+ ) +} + +export default AutoCarousel diff --git a/web/src/components/Project/EditProjectCell/EditProjectCell.tsx b/web/src/components/Project/EditProjectCell/EditProjectCell.tsx index 0a88921..01d2f8c 100644 --- a/web/src/components/Project/EditProjectCell/EditProjectCell.tsx +++ b/web/src/components/Project/EditProjectCell/EditProjectCell.tsx @@ -61,7 +61,7 @@ export const Success = ({ project }: CellSuccessProps) => { { onCompleted: () => { toast.success('Project updated') - navigate(routes.projects()) + navigate(routes.adminProjects()) }, onError: (error) => toast.error(error.message), } diff --git a/web/src/components/Project/NewProject/NewProject.tsx b/web/src/components/Project/NewProject/NewProject.tsx index da57dc5..6db2ef0 100644 --- a/web/src/components/Project/NewProject/NewProject.tsx +++ b/web/src/components/Project/NewProject/NewProject.tsx @@ -28,7 +28,7 @@ const NewProject = () => { { onCompleted: () => { toast.success('Project created') - navigate(routes.projects()) + navigate(routes.adminProjects()) }, onError: (error) => toast.error(error.message), } diff --git a/web/src/components/Project/Project/Project.tsx b/web/src/components/Project/Project/Project.tsx index 7290f64..da96a4e 100644 --- a/web/src/components/Project/Project/Project.tsx +++ b/web/src/components/Project/Project/Project.tsx @@ -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) => (
{
{fileIds.length > 0 ? ( <> - {fileIds.map((fileId, i) => ( -
-
- {i.toString()} -
-
-
- + {!appendUploader && + fileIds.map((fileId, i) => ( +
+
+
+ {i.toString()} +
+
+
+ +
+
-
- ))} + ))} {appendUploader && ( { const actionButtons = ( <> @@ -96,7 +96,7 @@ const ProjectsList = ({ projects }: FindProjects) => { {project.tags.map((tag, i) => (
( +
+ {projects + .slice() + .sort((a, b) => (isAfter(new Date(b.date), new Date(a.date)) ? 1 : -1)) + .map((project, i) => ( + +
+ {project.images.length > 0 && ( + + )} +
+
+

{project.title}

+
+
{project.description}
+
+
+ {isAfter(new Date(project.date), startOfToday()) && ( +
planned
+ )} +
+ {format(project.date, 'yyyy-MM-dd')} +
+
+
+ {project.tags.map((tag, i) => ( +
0.5 + ? 'black' + : 'white', + }} + > + {tag.tag} +
+ ))} +
+
+
+
+ + ))} +
+) + +export default ProjectsShowcase diff --git a/web/src/components/Project/ProjectsShowcaseCell/ProjectsShowcaseCell.tsx b/web/src/components/Project/ProjectsShowcaseCell/ProjectsShowcaseCell.tsx new file mode 100644 index 0000000..c40c1f6 --- /dev/null +++ b/web/src/components/Project/ProjectsShowcaseCell/ProjectsShowcaseCell.tsx @@ -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 = + gql` + query FindProjects { + projects { + id + title + description + images + date + links + tags { + id + tag + color + } + } + } + ` + +export const Loading = () => +export const Empty = () => +export const Failure = ({ error }: CellFailureProps) => ( + +) + +export const Success = ({ + projects, +}: CellSuccessProps) => ( + +) diff --git a/web/src/components/Tag/Tag/Tag.tsx b/web/src/components/Tag/Tag/Tag.tsx index f00ede2..67016a5 100644 --- a/web/src/components/Tag/Tag/Tag.tsx +++ b/web/src/components/Tag/Tag/Tag.tsx @@ -65,7 +65,7 @@ const Tag = ({ tag }: Props) => { Color
{ {truncate(tag.tag)}
{ 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', diff --git a/web/src/pages/ProjectPage/ProjectPage.tsx b/web/src/pages/ProjectPage/ProjectPage.tsx new file mode 100644 index 0000000..dd3bfb7 --- /dev/null +++ b/web/src/pages/ProjectPage/ProjectPage.tsx @@ -0,0 +1,27 @@ +import { Metadata } from '@redwoodjs/web' + +interface ProjectPageProps { + id: number +} + +// TODO: implement + +const ProjectPage = ({ id }: ProjectPageProps) => { + return ( + <> + + +

ProjectPage

+

+ Find me in ./web/src/pages/ProjectPage/ProjectPage.tsx +

+

My id is: {id}

+ {/* + My default route is named `project`, link to me with: + `Project` + */} + + ) +} + +export default ProjectPage diff --git a/web/src/pages/ProjectsPage/ProjectsPage.tsx b/web/src/pages/ProjectsPage/ProjectsPage.tsx new file mode 100644 index 0000000..34306ce --- /dev/null +++ b/web/src/pages/ProjectsPage/ProjectsPage.tsx @@ -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 ( + <> + + +
+
+
+

Projects

+

+ {isBrowser && !isMobile ? 'Click' : 'Tap'} on a project for + details +

+
+
+
+ + + + ) +} + +export default ProjectsPage diff --git a/yarn.lock b/yarn.lock index c855f47..c58e91b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15476,6 +15476,18 @@ __metadata: languageName: node linkType: hard +"react-device-detect@npm:^2.2.3": + version: 2.2.3 + resolution: "react-device-detect@npm:2.2.3" + dependencies: + ua-parser-js: "npm:^1.0.33" + peerDependencies: + react: ">= 0.14.0" + react-dom: ">= 0.14.0" + checksum: 10c0/396bbeeab0cb21da084c67434d204c9cf502fad6c683903313084d3f6487950a36a34f9bf67ccf5c1772a1bb5b79a2a4403fcfe6b51d93877db4c2d9f3a3a925 + languageName: node + linkType: hard + "react-dom@npm:18.3.1": version: 18.3.1 resolution: "react-dom@npm:18.3.1" @@ -17667,6 +17679,15 @@ __metadata: languageName: node linkType: hard +"ua-parser-js@npm:^1.0.33": + version: 1.0.39 + resolution: "ua-parser-js@npm:1.0.39" + bin: + ua-parser-js: script/cli.js + checksum: 10c0/c6452b0c683000f10975cb0a7e74cb1119ea95d4522ae85f396fa53b0b17884358a24ffdd86a66030c6b2981bdc502109a618c79fdaa217ee9032c9e46fcc78a + languageName: node + linkType: hard + "ua-parser-js@npm:^1.0.35": version: 1.0.38 resolution: "ua-parser-js@npm:1.0.38" @@ -18167,6 +18188,7 @@ __metadata: postcss-loader: "npm:^8.1.1" react: "npm:18.3.1" react-colorful: "npm:^5.6.1" + react-device-detect: "npm:^2.2.3" react-dom: "npm:18.3.1" tailwindcss: "npm:^3.4.8" languageName: unknown