Skip to content

Commit

Permalink
Added community page
Browse files Browse the repository at this point in the history
  • Loading branch information
Kevin-Umali committed Oct 13, 2023
1 parent 7f71c49 commit 7af3e48
Show file tree
Hide file tree
Showing 15 changed files with 310 additions and 12 deletions.
63 changes: 63 additions & 0 deletions client/app/community/community.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"use client";

import { useEffect, useState } from "react";
import { CommunityIdeaData } from "@/interfaces";

import { getCommunityGeneratedIdea } from "@/lib/index";
import { Label } from "@/components/ui/label";
import { Skeleton } from "@/components/ui/skeleton";
import { useToast } from "@/components/ui/use-toast";
import ProjectCard from "@/components/community/diy-card";

export default function CommunityGeneratedIdeaList() {
const [communityData, setCommunityData] = useState<CommunityIdeaData[]>([]);
const [loading, setLoading] = useState(true);
const { toast } = useToast();

useEffect(() => {
const fetchCommunityGeneratedIdeas = async () => {
try {
const fetchedCommunityGeneratedIdea = await getCommunityGeneratedIdea();
setCommunityData(fetchedCommunityGeneratedIdea.data);
} catch (error) {
console.error(error);
toast({
title: "Oops!",
description: "Failed to fetch community generated ideas. Please try again later.",
});
} finally {
setLoading(false);
}
};

fetchCommunityGeneratedIdeas();
}, [toast]);

return (
<div className="container mx-auto px-5 py-12 sm:px-10">
<div className="mb-12 text-center">
<h1 className="mb-3 text-3xl font-semibold lg:text-4xl">&ldquo;MakeMeDIYspire&rdquo; Community Generated Idea</h1>
<Label className="sm:text-md mt-2 inline-block text-sm">
Explore a myriad of innovative DIY ideas generated by our creative community members. Dive into the inspiration behind each project and kickstart your next DIY journey.
</Label>
</div>

<div className="grid grid-cols-1 gap-6 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
{loading
? [...Array(3)].map((_, i) => (
<div key={i}>
<Skeleton className="mb-5 h-10 w-full" />
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-full" />
</div>
<Skeleton className="mt-5 h-4 w-3/5" />
</div>
))
: communityData.map(({ id, title, description, tags, projectImage, createdAt }) => (
<ProjectCard key={id} id={id} title={title} description={description} badges={tags} imgUrl={projectImage.urls.small} createdAt={createdAt} />
))}
</div>
</div>
);
}
5 changes: 5 additions & 0 deletions client/app/community/loading.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import PageLoader from "@/components/page-loader";

export default function Loading() {
return <PageLoader loaderMessage="Loading community generated ideas" />;
}
25 changes: 25 additions & 0 deletions client/app/community/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { Metadata } from "next";

import CommunityGeneratedIdeaList from "./community";

export const metadata: Metadata = {
title: "MakeMeDIYspire Community Generated Ideas",
description:
"Join MakeMeDIYspire and discover an ever-growing list of DIY projects and ideas, all generated by our vibrant community of crafting enthusiasts. Dive into innovative DIY adventures and ignite your creativity with unique, user-driven project concepts.",
keywords: [
"Community-Generated DIY Ideas",
"MakeMeDIYspire Projects",
"User-Created Crafting Ideas",
"DIY Community Inspiration",
"Innovative DIY Concepts",
"Crafting Community",
"DIY Idea List",
"Creative Project Inspiration",
],
metadataBase: new URL("https://www.diyspire/community"),
applicationName: "MakeMeDIYspire",
};

export default function Page() {
return <CommunityGeneratedIdeaList />;
}
2 changes: 1 addition & 1 deletion client/app/guide/[guide_name]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export async function generateMetadata({ params }: { params: { guide_name: strin
const guide = await getGuideByPathMetadata(params.guide_name);

return {
title: guide.data.metadata.title,
title: `How to make ${guide.data.metadata.title}`,
description: guide.data.metadata.description,
keywords: ["DIY How-to Guides", "MakeMeDIYspire Tutorials", "DIY Project Instructions", "Step-by-Step DIY", "DIY Project Help", "DIY Creation Guide", "DIY Project Steps"],
metadataBase: new URL("https://www.diyspire/guide/" + guide.data.path),
Expand Down
10 changes: 2 additions & 8 deletions client/app/guide/guide.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,16 @@

import { useEffect, useState } from "react";
import Link from "next/link";
import { GuideData } from "@/interfaces";

import { getAllGuides } from "@/lib/index";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
import { Skeleton } from "@/components/ui/skeleton";
import { useToast } from "@/components/ui/use-toast";

type Guide = {
path: string;
metadata: {
title: string;
};
};

export default function HowToGuidesList() {
const [guides, setGuides] = useState<Guide[]>([]);
const [guides, setGuides] = useState<GuideData[]>([]);
const [loading, setLoading] = useState(true);
const { toast } = useToast();

Expand Down
63 changes: 63 additions & 0 deletions client/components/community/diy-card.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Image from "next/image";
import Link from "next/link";
import { BookOpen } from "lucide-react";

import { Card, CardContent, CardFooter, CardHeader } from "@/components/ui/card";

import { Badge } from "../ui/badge";
import { Button } from "../ui/button";
import { Label } from "../ui/label";

interface ProjectCardProps {
id: string | number;
title: string;
createdAt: string;
description: string;
imgUrl: string;
badges: string[];
}

const ProjectCard: React.FC<ProjectCardProps> = ({ id, title, createdAt, description, imgUrl, badges }) => {
return (
<Card className="mx-auto flex h-full max-w-md flex-col" role="article" aria-labelledby={`projectTitle-${id}`} aria-describedby={`projectDescription-${id}`}>
<div className="relative h-64">
<Image src={imgUrl} alt={title} layout="fill" objectFit="cover" className="absolute left-0 top-0 h-full w-full" loading="eager" />
</div>
<CardHeader className="flex flex-grow flex-col">
<h2 className="line-clamp-2 text-xl" role="heading" id={`projectTitle-${id}`}>
How to make {title}
</h2>
<Label className="line-clamp-3 text-sm" id={`projectDescription-${id}`}>
{description}
</Label>
</CardHeader>
<CardContent role="contentinfo" aria-label="Additional information">
<div className="mb-2 space-x-2" role="list" aria-label="Project badges">
{badges?.slice(0, 3).map((badge) => (
<Badge key={badge} className="mb-1 mr-1" role="listitem">
{badge}
</Badge>
))}
</div>
<Label aria-live="polite">
Created at:{" "}
{new Date(createdAt).toLocaleString("default", {
year: "numeric",
month: "long",
day: "numeric",
})}
</Label>
</CardContent>
<CardFooter className="mt-auto pt-2" role="contentinfo" aria-label="Footer section">
<Link className="w-full" href={{ pathname: `/project-detail/${id}` }} aria-label={`Read more about How to make ${title}`} passHref>
<Button className="w-full" aria-label={`Read more about ${title}`}>
<BookOpen className="mr-2 h-4 w-4" aria-label={`Read more about ${title}`} />
<span>Read More</span>
</Button>
</Link>
</CardFooter>
</Card>
);
};

export default ProjectCard;
1 change: 1 addition & 0 deletions client/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ export const footerData: Footer[] = [
links: [
{ label: "Generate DIY Project Idea", path: "/" },
{ label: "How-to Guides", path: "/guide" },
{ label: "Community", path: "/community" },
],
},
{
Expand Down
19 changes: 19 additions & 0 deletions client/interfaces/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,3 +219,22 @@ export interface ShareLinkResponse {
export interface ShareLinkResponseData {
id: string;
}

export interface CommunityIdeaResponse {
success: boolean;
data: CommunityIdeaData[];
}

export interface CommunityIdeaData {
id: number;
title: string;
description: string;
tags: string[];
projectImage: CommunityIdeaProjectImages;
createdAt: string;
}

export interface CommunityIdeaProjectImages {
urls: ProjectImageUrls;
alt_description?: string;
}
5 changes: 5 additions & 0 deletions client/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {
CommunityIdeaResponse,
CounterResponse,
GeneratedIdeaResponse,
GuidePathMetadataResponse,
Expand Down Expand Up @@ -108,6 +109,10 @@ export const incrementCounterOfGeneratedIdea = async (): Promise<any> => {
return fetchApi("/v1/counter", { method: "POST" });
};

export const getCommunityGeneratedIdea = async (limit = 20, orderBy = "desc"): Promise<CommunityIdeaResponse> => {
return fetchApi(`/v1/community?limit=${limit}&orderBy=${orderBy}`);
};

export const cleanMarkdown = (content: string): string => {
return content
.split("\n")
Expand Down
3 changes: 2 additions & 1 deletion server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ model Metadata {
}

model ProjectShareLink {
id String @id @default(uuid())
id String @id @default(uuid())
projectDetails Json
projectImage Json
explanation String?
createdAt DateTime @default(now())
}

model IdeaGenerationCounter {
Expand Down
67 changes: 67 additions & 0 deletions server/src/controllers/community.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import { Request, Response, NextFunction } from "express";
import { sendSuccess } from "../utils/response-template";
import { PrismaClient } from "@prisma/client";
import { parsePrisma, removeDuplicates, validateQueryParams } from "../utils";

export const getCommunityGeneratedIdea = async (req: Request, res: Response, next: NextFunction) => {
try {
const { limit, orderBy } = req.query;

const { validLimit, validOrderBy } = validateQueryParams(limit as string | undefined, orderBy as string | undefined);

const prisma = req.app.get("prisma") as PrismaClient;

const projects = await prisma.projectShareLink.findMany({
select: {
id: true,
projectDetails: true,
projectImage: true,
createdAt: true,
},
orderBy: {
createdAt: validOrderBy,
},
take: validLimit,
});

if (projects.length <= 0) {
sendSuccess(res, { message: "No community project exist" }, 404);
return;
}

const formattedProject = projects.map(({ id, projectDetails, projectImage, createdAt }) => {
const { urls, alt_description } = parsePrisma<{
urls: {
raw: string;
full: string;
small: string;
thumb: string;
regular: string;
small_s3: string;
};
alt_description: string;
}>(projectImage);
const { title, description, tags } = parsePrisma<{
title: string;
description: string;
tags: string[];
}>(projectDetails);

return {
id,
title,
description,
tags,
projectImage: {
urls,
alt_description,
},
createdAt,
};
});

return sendSuccess(res, removeDuplicates(formattedProject, ["title"]));
} catch (error) {
next(error);
}
};
8 changes: 8 additions & 0 deletions server/src/routes/community.routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import express from "express";
import { getCommunityGeneratedIdea } from "../controllers/community.controller";

const router = express.Router();

router.get("", getCommunityGeneratedIdea);

export default router;
4 changes: 3 additions & 1 deletion server/src/routes/index.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import unsplashRoutes from "./unsplash.routes";
import guideRoutes from "./guide.routes";
import shareRoutes from "./share.routes";
import counterRoutes from "./counter.routes";
import communityRoutes from "./community.routes";

const router = express.Router();

Expand All @@ -12,5 +13,6 @@ router.use("/image", unsplashRoutes);
router.use("/guide", guideRoutes);
router.use("/share", shareRoutes);
router.use("/counter", counterRoutes);
router.use("/community", communityRoutes);

export { openaiRoutes, unsplashRoutes, guideRoutes, shareRoutes, counterRoutes };
export { openaiRoutes, unsplashRoutes, guideRoutes, shareRoutes, counterRoutes, communityRoutes };
3 changes: 2 additions & 1 deletion server/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createApi } from "unsplash-js";
import * as nodeFetch from "node-fetch";
import { PrismaClient } from "@prisma/client";

import { guideRoutes, unsplashRoutes, openaiRoutes, shareRoutes, counterRoutes } from "./routes/index.routes";
import { guideRoutes, unsplashRoutes, openaiRoutes, shareRoutes, counterRoutes, communityRoutes } from "./routes/index.routes";
import { sendError, sendSuccess } from "./utils/response-template";
import errorHandlerMiddleware from "./middleware/error-handler";
import limiter from "./middleware/request-limit";
Expand Down Expand Up @@ -71,6 +71,7 @@ app.get(
const oneDayCacheMiddleware = getConditionalCache("24 hours");
app.use("/api/v1/guide", oneDayCacheMiddleware, guideRoutes);
app.use("/api/v1/image", oneDayCacheMiddleware, unsplashRoutes);
app.use("/api/v1/community", oneDayCacheMiddleware, communityRoutes);

const oneYearCacheMiddleware = getConditionalCache("12 months");
app.use("/api/v1/share", oneYearCacheMiddleware, shareRoutes);
Expand Down
Loading

0 comments on commit 7af3e48

Please sign in to comment.