From 5722c14df04327618e6bf486df3aeeb3292bc925 Mon Sep 17 00:00:00 2001 From: Kevin-Umali Date: Fri, 6 Oct 2023 23:48:29 +0800 Subject: [PATCH 1/4] Initial nextjs commit --- client-next/.eslintrc.json | 3 + client-next/.gitignore | 35 +++ client-next/README.md | 36 +++ client-next/app/faq/faq.tsx | 55 +++++ client-next/app/faq/page.tsx | 14 ++ client-next/app/globals.css | 59 +++++ .../app/guide/[guide_name]/guide-name.tsx | 85 ++++++++ client-next/app/guide/[guide_name]/page.tsx | 19 ++ client-next/app/guide/guide.tsx | 73 +++++++ client-next/app/guide/page.tsx | 14 ++ client-next/app/icon.ico | Bin 0 -> 15406 bytes client-next/app/layout.tsx | 60 +++++ client-next/app/page.tsx | 178 +++++++++++++++ client-next/app/robots.ts | 10 + client-next/components.json | 16 ++ client-next/components/custom-markdown.tsx | 16 ++ client-next/components/footer.tsx | 46 ++++ .../components/generate/budget-filter.tsx | 28 +++ .../components/generate/category-filter.tsx | 46 ++++ .../components/generate/difficulty-filter.tsx | 41 ++++ .../components/generate/generate-loading.tsx | 21 ++ .../components/generate/material-input.tsx | 71 ++++++ .../components/generate/project-tabs.tsx | 79 +++++++ .../components/generate/purpose-filter.tsx | 39 ++++ .../components/generate/safety-check.tsx | 22 ++ .../generate/time-availability-filter.tsx | 42 ++++ .../generate/tools-available-input.tsx | 53 +++++ client-next/components/navbar.tsx | 36 +++ client-next/components/theme-provider.tsx | 9 + client-next/components/ui/accordion.tsx | 60 +++++ client-next/components/ui/alert-dialog.tsx | 141 ++++++++++++ client-next/components/ui/alert.tsx | 59 +++++ client-next/components/ui/badge.tsx | 36 +++ client-next/components/ui/button.tsx | 56 +++++ client-next/components/ui/card.tsx | 79 +++++++ client-next/components/ui/checkbox.tsx | 30 +++ client-next/components/ui/dialog.tsx | 119 ++++++++++ client-next/components/ui/input.tsx | 25 +++ client-next/components/ui/label.tsx | 26 +++ client-next/components/ui/select.tsx | 121 ++++++++++ client-next/components/ui/separator.tsx | 31 +++ client-next/components/ui/skeleton.tsx | 15 ++ client-next/components/ui/tabs.tsx | 55 +++++ client-next/components/ui/toast.tsx | 78 +++++++ client-next/components/ui/toaster.tsx | 35 +++ client-next/components/ui/tooltip.tsx | 30 +++ client-next/components/ui/use-toast.ts | 192 ++++++++++++++++ client-next/constants/index.ts | 206 ++++++++++++++++++ client-next/hooks/useInterval.ts | 22 ++ client-next/interfaces/index.ts | 100 +++++++++ client-next/lib/index.ts | 93 ++++++++ client-next/lib/utils.ts | 6 + client-next/next.config.js | 4 + client-next/package.json | 47 ++++ client-next/postcss.config.js | 6 + client-next/public/android-chrome-192x192.png | Bin 0 -> 10911 bytes client-next/public/android-chrome-512x512.png | Bin 0 -> 28096 bytes client-next/public/apple-touch-icon.png | Bin 0 -> 9762 bytes client-next/public/favicon-16x16.png | Bin 0 -> 482 bytes client-next/public/favicon-32x32.png | Bin 0 -> 1150 bytes client-next/tailwind.config.ts | 71 ++++++ client-next/tsconfig.json | 27 +++ 62 files changed, 2976 insertions(+) create mode 100644 client-next/.eslintrc.json create mode 100644 client-next/.gitignore create mode 100644 client-next/README.md create mode 100644 client-next/app/faq/faq.tsx create mode 100644 client-next/app/faq/page.tsx create mode 100644 client-next/app/globals.css create mode 100644 client-next/app/guide/[guide_name]/guide-name.tsx create mode 100644 client-next/app/guide/[guide_name]/page.tsx create mode 100644 client-next/app/guide/guide.tsx create mode 100644 client-next/app/guide/page.tsx create mode 100644 client-next/app/icon.ico create mode 100644 client-next/app/layout.tsx create mode 100644 client-next/app/page.tsx create mode 100644 client-next/app/robots.ts create mode 100644 client-next/components.json create mode 100644 client-next/components/custom-markdown.tsx create mode 100644 client-next/components/footer.tsx create mode 100644 client-next/components/generate/budget-filter.tsx create mode 100644 client-next/components/generate/category-filter.tsx create mode 100644 client-next/components/generate/difficulty-filter.tsx create mode 100644 client-next/components/generate/generate-loading.tsx create mode 100644 client-next/components/generate/material-input.tsx create mode 100644 client-next/components/generate/project-tabs.tsx create mode 100644 client-next/components/generate/purpose-filter.tsx create mode 100644 client-next/components/generate/safety-check.tsx create mode 100644 client-next/components/generate/time-availability-filter.tsx create mode 100644 client-next/components/generate/tools-available-input.tsx create mode 100644 client-next/components/navbar.tsx create mode 100644 client-next/components/theme-provider.tsx create mode 100644 client-next/components/ui/accordion.tsx create mode 100644 client-next/components/ui/alert-dialog.tsx create mode 100644 client-next/components/ui/alert.tsx create mode 100644 client-next/components/ui/badge.tsx create mode 100644 client-next/components/ui/button.tsx create mode 100644 client-next/components/ui/card.tsx create mode 100644 client-next/components/ui/checkbox.tsx create mode 100644 client-next/components/ui/dialog.tsx create mode 100644 client-next/components/ui/input.tsx create mode 100644 client-next/components/ui/label.tsx create mode 100644 client-next/components/ui/select.tsx create mode 100644 client-next/components/ui/separator.tsx create mode 100644 client-next/components/ui/skeleton.tsx create mode 100644 client-next/components/ui/tabs.tsx create mode 100644 client-next/components/ui/toast.tsx create mode 100644 client-next/components/ui/toaster.tsx create mode 100644 client-next/components/ui/tooltip.tsx create mode 100644 client-next/components/ui/use-toast.ts create mode 100644 client-next/constants/index.ts create mode 100644 client-next/hooks/useInterval.ts create mode 100644 client-next/interfaces/index.ts create mode 100644 client-next/lib/index.ts create mode 100644 client-next/lib/utils.ts create mode 100644 client-next/next.config.js create mode 100644 client-next/package.json create mode 100644 client-next/postcss.config.js create mode 100644 client-next/public/android-chrome-192x192.png create mode 100644 client-next/public/android-chrome-512x512.png create mode 100644 client-next/public/apple-touch-icon.png create mode 100644 client-next/public/favicon-16x16.png create mode 100644 client-next/public/favicon-32x32.png create mode 100644 client-next/tailwind.config.ts create mode 100644 client-next/tsconfig.json diff --git a/client-next/.eslintrc.json b/client-next/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/client-next/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/client-next/.gitignore b/client-next/.gitignore new file mode 100644 index 0000000..8f322f0 --- /dev/null +++ b/client-next/.gitignore @@ -0,0 +1,35 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/client-next/README.md b/client-next/README.md new file mode 100644 index 0000000..c403366 --- /dev/null +++ b/client-next/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/client-next/app/faq/faq.tsx b/client-next/app/faq/faq.tsx new file mode 100644 index 0000000..ac7e12c --- /dev/null +++ b/client-next/app/faq/faq.tsx @@ -0,0 +1,55 @@ +"use client"; + +import Link from "next/link"; +import { useEffect, useState } from "react"; +import { faqs } from "@/constants"; +import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"; +import { FAQLinkData } from "@/interfaces"; + +export default function FAQPage() { + const [accordionDefaultValue, setAccordionDefaultValue] = useState(`item-0`); + + const parseAnswer = (answer: string | undefined, links?: { [key: string]: FAQLinkData }) => { + if (!answer) return; + + return answer.split(/\{(.+?)\}/g).map((segment, index) => + index % 2 === 0 ? ( + segment + ) : ( + + {links?.[segment]?.text ?? segment} + + ) + ); + }; + + useEffect(() => { + const hash = window.location.hash; + if (!hash) return; + + const hashValue = hash.split("#").filter(Boolean)[0]; + const index = faqs.findIndex((faq) => faq.id == hashValue); + if (index !== -1) { + setAccordionDefaultValue(`item-${index}`); + } + }, []); + + return ( + <> +
+
+

Frequently Asked Questions

+
+ + + {faqs.map((faq, index) => ( + + {faq.question} + {faq.links ? parseAnswer(faq.answer, faq.links) : faq.answer} + + ))} + +
+ + ); +} diff --git a/client-next/app/faq/page.tsx b/client-next/app/faq/page.tsx new file mode 100644 index 0000000..77c999a --- /dev/null +++ b/client-next/app/faq/page.tsx @@ -0,0 +1,14 @@ +import { Metadata } from "next"; +import FAQPage from "./faq"; + +export const metadata: Metadata = { + title: "MakeMeDIYspire FAQs", + description: "Discover answers to all your queries about the MakeMeDIYspire platform, its functionality, feedback process, and more.", + keywords: ["DIY FAQs", "MakeMeDIYspire Questions", "DIY Project Help", "DIY Platform Queries", "Project Generator FAQs", "DIY Project Creation", "DIY Inspiration"], + metadataBase: new URL("https://www.diyspire/faq"), + applicationName: "MakeMeDIYspire", +}; + +export default function Page() { + return ; +} diff --git a/client-next/app/globals.css b/client-next/app/globals.css new file mode 100644 index 0000000..b486a0a --- /dev/null +++ b/client-next/app/globals.css @@ -0,0 +1,59 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 5.9% 10%; + --radius: 0rem; + } + + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/client-next/app/guide/[guide_name]/guide-name.tsx b/client-next/app/guide/[guide_name]/guide-name.tsx new file mode 100644 index 0000000..ab257e3 --- /dev/null +++ b/client-next/app/guide/[guide_name]/guide-name.tsx @@ -0,0 +1,85 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { getGuideByPath } from "@/lib/index"; +import { HowToGuide } from "@/interfaces"; +import { useRouter } from "next/navigation"; +import Head from "next/head"; +import CustomMarkdown from "@/components/custom-markdown"; +import { useToast } from "@/components/ui/use-toast"; +import { Skeleton } from "@/components/ui/skeleton"; + +export default function HowToGuideDetail({ params }: { params: { guide_name: string } }) { + const router = useRouter(); + + const [guideDetails, setGuideDetails] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const { toast } = useToast(); + + useEffect(() => { + if (!params.guide_name) { + toast({ + title: "Oops!", + description: "Not found", + }); + + router.push("/guide"); + return; + } + + const fetchGuide = async () => { + try { + const fetchedGuide = await getGuideByPath(params.guide_name); + setGuideDetails(fetchedGuide.data); + } catch (error: any) { + console.error(error); + toast({ + title: "Oops!", + description: error.message, + }); + router.push("/guide"); + } finally { + setIsLoading(false); + } + }; + + fetchGuide(); + }, [params.guide_name, router, toast]); + + return ( +
+ + {guideDetails && ( + <> + {guideDetails.metadata.title} + + + )} + + + {isLoading ? ( +
+ + {[...Array(5)].map((_, i) => ( +
+ +
+ ))} +
+ ) : ( + <> +
+

“MakeMeDIYspire” Guides

+
+ + {guideDetails && ( +
+ +
+ )} + + )} +
+ ); +} diff --git a/client-next/app/guide/[guide_name]/page.tsx b/client-next/app/guide/[guide_name]/page.tsx new file mode 100644 index 0000000..ff957a9 --- /dev/null +++ b/client-next/app/guide/[guide_name]/page.tsx @@ -0,0 +1,19 @@ +import { Metadata, ResolvingMetadata } from "next"; +import { getGuideByPath } from "@/lib"; +import HowToGuideDetail from "./guide-name"; + +export async function generateMetadata({ params }: { params: { guide_name: string } }, parent: ResolvingMetadata): Promise { + const guide = await getGuideByPath(params.guide_name); + + return { + title: 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), + applicationName: "MakeMeDIYspire", + }; +} + +export default function Page({ params }: { params: { guide_name: string } }) { + return ; +} diff --git a/client-next/app/guide/guide.tsx b/client-next/app/guide/guide.tsx new file mode 100644 index 0000000..7059de3 --- /dev/null +++ b/client-next/app/guide/guide.tsx @@ -0,0 +1,73 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { getAllGuides } from "@/lib/index"; +import { Separator } from "@/components/ui/separator"; +import { Skeleton } from "@/components/ui/skeleton"; +import { useToast } from "@/components/ui/use-toast"; +import { Label } from "@/components/ui/label"; + +type Guide = { + path: string; + metadata: { + title: string; + }; +}; + +export default function HowToGuidesList() { + const [guides, setGuides] = useState([]); + const [loading, setLoading] = useState(true); + const { toast } = useToast(); + + useEffect(() => { + const fetchGuides = async () => { + try { + const fetchedGuides = await getAllGuides(); + setGuides(fetchedGuides.data); + } catch (error) { + console.error(error); + toast({ + title: "Oops!", + description: "Failed to fetch guides. Please try again later.", + }); + } finally { + setLoading(false); + } + }; + + fetchGuides(); + }, [toast]); + + return ( + <> +
+
+

“MakeMeDIYspire” Guides

+ +
+ + {loading ? ( + [...Array(2)].map((_, i) => ( +
+
+ + +
+ {i !== guides.length - 1 && } +
+ )) + ) : ( +
    + {guides.map((guide, index) => ( +
  • + {guide.metadata.title} + {index !== guides.length - 1 && } +
  • + ))} +
+ )} +
+ + ); +} diff --git a/client-next/app/guide/page.tsx b/client-next/app/guide/page.tsx new file mode 100644 index 0000000..6c0252e --- /dev/null +++ b/client-next/app/guide/page.tsx @@ -0,0 +1,14 @@ +import HowToGuidesList from "./guide"; +import { Metadata } from "next"; + +export const metadata: Metadata = { + title: "MakeMeDIYspire How-to Guides", + description: "Navigate through our comprehensive guides and maximize the potential of the MakeMeDIYspire DIY project generator.", + keywords: ["DIY Project Guides", "MakeMeDIYspire Instructions", "DIY Project Tips", "DIY Crafting Guide", "DIY Project Creation", "DIY Generator Guide", "DIY Inspiration"], + metadataBase: new URL("https://www.diyspire/guide"), + applicationName: "MakeMeDIYspire", +}; + +export default function Page() { + return ; +} diff --git a/client-next/app/icon.ico b/client-next/app/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..13bc858f425dde6b3e07e0465e14460e2eb19dd8 GIT binary patch literal 15406 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 literal 0 HcmV?d00001 diff --git a/client-next/app/layout.tsx b/client-next/app/layout.tsx new file mode 100644 index 0000000..c3e346a --- /dev/null +++ b/client-next/app/layout.tsx @@ -0,0 +1,60 @@ +import { ThemeProvider } from "@/components/theme-provider"; +import "./globals.css"; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; +import Navbar from "@/components/navbar"; +import Footer from "@/components/footer"; +import { Toaster } from "@/components/ui/toaster"; + +const inter = Inter({ subsets: ["latin"] }); + +export const metadata: Metadata = { + title: "MakeMeDIYspire - AI-Powered DIY Project Idea Generator", + description: "Unleash a world of unique DIY project ideas with MakeMeDIYspire, an AI-powered generator backed by OpenAI. Ignite your creativity and embark on your next inventive journey.", + keywords: [ + "AI DIY Projects", + "OpenAI Project Ideas", + "AI-Powered DIY Generator", + "Creative Project Ideas", + "DIY Inspiration", + "Automated DIY Ideas", + "Innovative Project Generator", + "DIY Creativity", + ], + metadataBase: new URL("https://www.diyspire.online/"), + applicationName: "MakeMeDIYspire", + openGraph: { + type: "website", + url: "https://www.diyspire.online/", + title: "MakeMeDIYspire - AI-Powered DIY Project Idea Generator", + description: "Unleash a world of unique DIY project ideas with MakeMeDIYspire, an AI-powered generator backed by OpenAI. Ignite your creativity and embark on your next inventive journey.", + }, + twitter: { + site: "https://www.diyspire.online/", + title: "MakeMeDIYspire - AI-Powered DIY Project Idea Generator", + description: "Unleash a world of unique DIY project ideas with MakeMeDIYspire, an AI-powered generator backed by OpenAI. Ignite your creativity and embark on your next inventive journey.", + }, + themeColor: [{ color: "#ffffff" }], + colorScheme: "light", + referrer: "no-referrer-when-downgrade", + formatDetection: { + telephone: false, + }, +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + +
+ +
{children}
+
+ +
+
+ + + ); +} diff --git a/client-next/app/page.tsx b/client-next/app/page.tsx new file mode 100644 index 0000000..2baafbd --- /dev/null +++ b/client-next/app/page.tsx @@ -0,0 +1,178 @@ +"use client"; + +import React, { useCallback, useEffect, useMemo, useState } from "react"; +import BudgetFilter from "@/components/generate/budget-filter"; +import CategoryFilter from "@/components/generate/category-filter"; +import DifficultyFilter from "@/components/generate/difficulty-filter"; +import GenerateLoading from "@/components/generate/generate-loading"; +import MaterialInput from "@/components/generate/material-input"; +import ProjectTabs from "@/components/generate/project-tabs"; +import SafetyCheck from "@/components/generate/safety-check"; +import TimeAvailabilityFilter from "@/components/generate/time-availability-filter"; +import ToolsAvailableInput from "@/components/generate/tools-available-input"; +import { categories } from "@/constants"; +import { Button } from "@/components/ui/button"; +import PurposeFilter from "@/components/generate/purpose-filter"; +import { RefreshCcw } from "lucide-react"; +import { generateProjectIdeas, getTotalCountOfGeneratedIdea, incrementCounterOfGeneratedIdea } from "@/lib"; +import { useToast } from "@/components/ui/use-toast"; +import { Label } from "@/components/ui/label"; + +export default function Home() { + const [materials, setMaterials] = useState([""]); + const [onlySpecified, setOnlySpecified] = useState(false); + const [selectedDifficulty, setSelectedDifficulty] = useState("All"); + const [selectedCategory, setSelectedCategory] = useState("Anything"); + const [timeValue, setTimeValue] = useState(0); + const [timeUnit, setTimeUnit] = useState(null); + const [budget, setBudget] = useState(0); + const [tools, setTools] = useState([""]); + const [purpose, setPurpose] = useState("Personal Use"); + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); + const [isSafe, setIsSafe] = useState(false); + + const [isGenerating, setIsGenerating] = useState(false); + const [isGenerated, setIsGenerated] = useState(false); + + const [projects, setProjects] = useState([]); + + const [totalCount, setTotalCount] = useState(0); + const [isLoadingCount, setIsLoadingCount] = useState(false); + + const { toast } = useToast(); + + const toggleAdvancedOptions = () => { + setShowAdvancedOptions(!showAdvancedOptions); + }; + + useEffect(() => { + const fetchTotalCount = async () => { + setIsLoadingCount(true); + try { + const response = await getTotalCountOfGeneratedIdea(); + if (response?.data?.totalCount) { + setTotalCount(response.data.totalCount); + } + } catch (error) { + console.error(error); + toast({ + title: "Oops!", + description: "Failed to fetch the total count of generated ideas. Please try again later.", + }); + } finally { + setIsLoadingCount(false); + } + }; + + fetchTotalCount(); + }, [toast]); + + const handleGenerateProjects = useCallback(async () => { + try { + if (!isSafe) throw new Error("Please check the safety checkbox"); + + setIsGenerating(true); + + const response = await generateProjectIdeas(materials, onlySpecified, selectedDifficulty, selectedCategory, tools, timeValue, timeUnit, budget, purpose); + + if (response.data?.ideas) { + setProjects(response.data.ideas); + await incrementCounterOfGeneratedIdea(); + setTotalCount((count) => count + 1); + setIsGenerated(true); + } + } catch (error: any) { + console.error(error); + toast({ + title: "Error", + description: error.message, + }); + } finally { + setIsGenerating(false); + } + }, [budget, isSafe, materials, onlySpecified, purpose, selectedCategory, selectedDifficulty, timeUnit, timeValue, tools, toast]); + + const advancedOptions = useMemo( + () => ( + <> +
+ +
+ +
+ +
+ +
+ +
+ +
+ +
+ + ), + [purpose, timeUnit, timeValue, tools] + ); + + const renderContent = () => { + if (isGenerating) { + return ; + } + + if (isGenerated) { + return ( +
+ + +
+ ); + } + + return ( + <> +
+ +
+
+ +
+
+ +
+ + {showAdvancedOptions && advancedOptions} +
+
+ +
+ +
+ + ); + }; + + return ( +
+

DIY Project Ideas

+
+ +
+ {renderContent()} +
+ ); +} diff --git a/client-next/app/robots.ts b/client-next/app/robots.ts new file mode 100644 index 0000000..fc42c63 --- /dev/null +++ b/client-next/app/robots.ts @@ -0,0 +1,10 @@ +import { MetadataRoute } from "next"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: { + userAgent: "*", + allow: "/", + }, + }; +} diff --git a/client-next/components.json b/client-next/components.json new file mode 100644 index 0000000..48c34e4 --- /dev/null +++ b/client-next/components.json @@ -0,0 +1,16 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.js", + "css": "app/globals.css", + "baseColor": "slate", + "cssVariables": true + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils" + } +} \ No newline at end of file diff --git a/client-next/components/custom-markdown.tsx b/client-next/components/custom-markdown.tsx new file mode 100644 index 0000000..22062ff --- /dev/null +++ b/client-next/components/custom-markdown.tsx @@ -0,0 +1,16 @@ +import { cleanMarkdown } from "@/lib"; +import ReactMarkdown from "react-markdown"; + +type CustomMarkdownProps = { + content: string; +}; + +const CustomMarkdown: React.FC = ({ content }) => { + return ( +
+ {cleanMarkdown(content)} +
+ ); +}; + +export default CustomMarkdown; diff --git a/client-next/components/footer.tsx b/client-next/components/footer.tsx new file mode 100644 index 0000000..4b07422 --- /dev/null +++ b/client-next/components/footer.tsx @@ -0,0 +1,46 @@ +import Link from "next/link"; +import { footerData } from "@/constants"; + +const Footer: React.FC = () => { + return ( +
+
+
+ {footerData.map((data, index) => ( +
+ + {data.label} + +
+ {data.links.map((link, linkIndex) => ( + + {link.label} + + ))} +
+
+ ))} +
+ +
+

+ Powered by MakeMeDIYspire ✨ | Made with ❤️ by -{" "} + + Kooma + +

+
+
+
+ ); +}; + +export default Footer; diff --git a/client-next/components/generate/budget-filter.tsx b/client-next/components/generate/budget-filter.tsx new file mode 100644 index 0000000..1a28a3b --- /dev/null +++ b/client-next/components/generate/budget-filter.tsx @@ -0,0 +1,28 @@ +import { useCallback } from "react"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; + +interface BudgetFilterProps { + onBudgetChange: (budget: number) => void; + className?: string; +} + +const BudgetFilter: React.FC = ({ onBudgetChange, className }) => { + const handleChange = useCallback( + (event: React.ChangeEvent) => { + onBudgetChange(Number(event.target.value)); + }, + [onBudgetChange] + ); + + return ( +
+ + +
+ ); +}; + +export default BudgetFilter; diff --git a/client-next/components/generate/category-filter.tsx b/client-next/components/generate/category-filter.tsx new file mode 100644 index 0000000..af480fe --- /dev/null +++ b/client-next/components/generate/category-filter.tsx @@ -0,0 +1,46 @@ +import React, { useState } from "react"; +import { categoryIcons } from "@/constants"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; + +interface CategoryFilterProps { + categories: string[]; + onCategoryChange: (selectedCategory: string) => void; + className?: string; +} + +const CategoryFilter: React.FC = ({ categories, onCategoryChange, className }) => { + const [selectedCategory, setSelectedCategory] = useState("Anything"); + + const handleCategoryClick = (category: string) => { + setSelectedCategory(category); + onCategoryChange(category); + }; + + return ( +
+
+ +
+
+ {categories.map((category) => { + const isSelected = selectedCategory === category; + const IconComponent = categoryIcons[category]; + return ( + + ); + })} +
+
+ ); +}; + +export default CategoryFilter; diff --git a/client-next/components/generate/difficulty-filter.tsx b/client-next/components/generate/difficulty-filter.tsx new file mode 100644 index 0000000..c6ae463 --- /dev/null +++ b/client-next/components/generate/difficulty-filter.tsx @@ -0,0 +1,41 @@ +import React, { useState } from "react"; +import { difficulties } from "@/constants"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; + +interface DifficultyFilterProps { + onDifficultyChange: (difficulty: string) => void; + className?: string; +} + +const DifficultyFilter: React.FC = ({ onDifficultyChange, className }) => { + const [selectedDifficulty, setSelectedDifficulty] = useState("all"); + + const handleDifficultyClick = (difficulty: string) => { + setSelectedDifficulty(difficulty); + onDifficultyChange(difficulty); + }; + + return ( +
+
+ +
+
+ {difficulties.map(({ level, icon: IconComponent }) => ( + + ))} +
+
+ ); +}; + +export default DifficultyFilter; diff --git a/client-next/components/generate/generate-loading.tsx b/client-next/components/generate/generate-loading.tsx new file mode 100644 index 0000000..d696dab --- /dev/null +++ b/client-next/components/generate/generate-loading.tsx @@ -0,0 +1,21 @@ +import { useState } from "react"; +import { loadingMessages } from "@/constants"; +import useInterval from "@/hooks/useInterval"; +import { Loader } from "lucide-react"; + +const GenerateLoading: React.FC = () => { + const [currentMessageIndex, setCurrentMessageIndex] = useState(0); + + useInterval(() => { + setCurrentMessageIndex((prevIndex) => (prevIndex + 1) % loadingMessages.length); + }, 5000); + + return ( +
+ +

{loadingMessages[currentMessageIndex]}

+
+ ); +}; + +export default GenerateLoading; diff --git a/client-next/components/generate/material-input.tsx b/client-next/components/generate/material-input.tsx new file mode 100644 index 0000000..e6e56f0 --- /dev/null +++ b/client-next/components/generate/material-input.tsx @@ -0,0 +1,71 @@ +import { useCallback } from "react"; +import { Plus, PlusCircle, X } from "lucide-react"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Input } from "@/components/ui/input"; + +interface MaterialInputProps { + materials: string[]; + setMaterials: React.Dispatch>; + onlySpecified: boolean; + setOnlySpecified: React.Dispatch>; + className?: string; +} + +const MaterialInput: React.FC = ({ materials, setMaterials, onlySpecified, setOnlySpecified, className }) => { + const handleInputChange = useCallback( + (index: number, value: string) => { + const updatedMaterials = [...materials]; + updatedMaterials[index] = value; + setMaterials(updatedMaterials); + }, + [materials, setMaterials] + ); + + const handleDeleteInput = useCallback( + (index: number) => { + const updatedMaterials = [...materials]; + updatedMaterials.splice(index, 1); + setMaterials(updatedMaterials); + }, + [materials, setMaterials] + ); + + const handleAddMore = useCallback(() => { + setMaterials([...materials, ""]); + }, [materials, setMaterials]); + + return ( +
+
+
+ + +
+ +
+ {materials.map((material, index) => ( +
+ handleInputChange(index, e.target.value)} /> + +
+ ))} + {materials.some((mat) => mat.trim() !== "") && ( +
+ + +
+ )} +
+ ); +}; + +export default MaterialInput; diff --git a/client-next/components/generate/project-tabs.tsx b/client-next/components/generate/project-tabs.tsx new file mode 100644 index 0000000..ffb0604 --- /dev/null +++ b/client-next/components/generate/project-tabs.tsx @@ -0,0 +1,79 @@ +import Link from "next/link"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Separator } from "@/components/ui/separator"; +import { Badge } from "@/components/ui/badge"; +import { Label } from "@/components/ui/label"; +import { Button } from "@/components/ui/button"; +import { BookOpen } from "lucide-react"; + +interface Project { + title: string; + materials: string[]; + tools: string[]; + time: string; + budget: string; + tags: string[]; + description: string; +} + +interface ProjectTabsProps { + projects: Project[]; + className?: string; +} + +const ProjectTabs: React.FC = ({ projects, className }) => { + return ( + + + {projects.map((project) => ( + + {project.title} + + ))} + + {projects.map((project) => ( + +
+

{project.title}

+ +
+ {project.tags.map((tag, tagIndex) => ( + + {tag} + + ))} +
+
+ +

{project.description}

+
+ + + + +
+ +
+ + + + +
+
+ ))} +
+ ); +}; + +export default ProjectTabs; diff --git a/client-next/components/generate/purpose-filter.tsx b/client-next/components/generate/purpose-filter.tsx new file mode 100644 index 0000000..3b85a5b --- /dev/null +++ b/client-next/components/generate/purpose-filter.tsx @@ -0,0 +1,39 @@ +import React, { useState } from "react"; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; + +interface PurposeFilterProps { + purpose: string; + onPurposeChange: (purpose: string) => void; +} + +const PurposeFilter: React.FC = ({ purpose, onPurposeChange }) => { + const [selectedPurpose, setSelectedPurpose] = useState(purpose); + + const handlePurposeChange = (newPurpose: string) => { + setSelectedPurpose(newPurpose); + onPurposeChange(newPurpose); + }; + + return ( +
+ + +
+ ); +}; + +export default PurposeFilter; diff --git a/client-next/components/generate/safety-check.tsx b/client-next/components/generate/safety-check.tsx new file mode 100644 index 0000000..5b83e55 --- /dev/null +++ b/client-next/components/generate/safety-check.tsx @@ -0,0 +1,22 @@ +import { Checkbox } from "@/components/ui/checkbox"; +import { Label } from "@/components/ui/label"; + +interface SafetyCheckFilterProps { + onSafetyConfirmation: (isSafe: boolean) => void; + className?: string; +} + +const SafetyCheck: React.FC = ({ onSafetyConfirmation, className }) => { + const handleChange = (checkedState: boolean) => { + onSafetyConfirmation(checkedState); + }; + + return ( +
+ + +
+ ); +}; + +export default SafetyCheck; diff --git a/client-next/components/generate/time-availability-filter.tsx b/client-next/components/generate/time-availability-filter.tsx new file mode 100644 index 0000000..b3302ec --- /dev/null +++ b/client-next/components/generate/time-availability-filter.tsx @@ -0,0 +1,42 @@ +import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { Label } from "@/components/ui/label"; +import { Input } from "@/components/ui/input"; + +interface TimeAvailabilityFilterProps { + timeValue: number; + timeUnit: string | null; + onValueChange: (value: number) => void; + onUnitChange: (unit: string) => void; + className?: string; +} + +const TimeAvailabilityFilter: React.FC = ({ timeValue, timeUnit, onValueChange, onUnitChange, className }) => { + return ( +
+ +
+ onValueChange(Number(e.target.value))} className="w-20" /> + +
+
+ ); +}; + +export default TimeAvailabilityFilter; diff --git a/client-next/components/generate/tools-available-input.tsx b/client-next/components/generate/tools-available-input.tsx new file mode 100644 index 0000000..208a8cb --- /dev/null +++ b/client-next/components/generate/tools-available-input.tsx @@ -0,0 +1,53 @@ +import { useCallback } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { PlusCircle, X } from "lucide-react"; + +interface ToolsAvailableInputProps { + tools: string[]; + setTools: React.Dispatch>; + className?: string; +} + +const ToolsAvailableInput: React.FC = ({ tools, setTools, className }) => { + const handleInputChange = useCallback( + (index: number, value: string) => { + const newTools = [...tools]; + newTools[index] = value; + setTools(newTools); + }, + [tools, setTools] + ); + + const handleDeleteTool = useCallback( + (index: number) => { + const newTools = [...tools]; + newTools.splice(index, 1); + setTools(newTools); + }, + [tools, setTools] + ); + + return ( +
+ +
+ {tools.map((tool, index) => ( +
+ handleInputChange(index, e.target.value)} /> + +
+ ))} + +
+
+ ); +}; + +export default ToolsAvailableInput; diff --git a/client-next/components/navbar.tsx b/client-next/components/navbar.tsx new file mode 100644 index 0000000..5bd43ed --- /dev/null +++ b/client-next/components/navbar.tsx @@ -0,0 +1,36 @@ +"use client"; + +import Link from "next/link"; +import { useTheme } from "next-themes"; +import { Sun, Moon, Github } from "lucide-react"; + +const Navbar: React.FC = () => { + const { theme, setTheme } = useTheme(); + + const toggleColorMode = () => { + setTheme(theme === "light" ? "dark" : "light"); + }; + + return ( + + ); +}; + +export default Navbar; diff --git a/client-next/components/theme-provider.tsx b/client-next/components/theme-provider.tsx new file mode 100644 index 0000000..b0ff266 --- /dev/null +++ b/client-next/components/theme-provider.tsx @@ -0,0 +1,9 @@ +"use client"; + +import * as React from "react"; +import { ThemeProvider as NextThemesProvider } from "next-themes"; +import { type ThemeProviderProps } from "next-themes/dist/types"; + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children}; +} diff --git a/client-next/components/ui/accordion.tsx b/client-next/components/ui/accordion.tsx new file mode 100644 index 0000000..937620a --- /dev/null +++ b/client-next/components/ui/accordion.tsx @@ -0,0 +1,60 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/client-next/components/ui/alert-dialog.tsx b/client-next/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..c7925a0 --- /dev/null +++ b/client-next/components/ui/alert-dialog.tsx @@ -0,0 +1,141 @@ +"use client" + +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/client-next/components/ui/alert.tsx b/client-next/components/ui/alert.tsx new file mode 100644 index 0000000..41fa7e0 --- /dev/null +++ b/client-next/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/client-next/components/ui/badge.tsx b/client-next/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/client-next/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/client-next/components/ui/button.tsx b/client-next/components/ui/button.tsx new file mode 100644 index 0000000..ac8e0c9 --- /dev/null +++ b/client-next/components/ui/button.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-10 px-4 py-2", + sm: "h-9 rounded-md px-3", + lg: "h-11 rounded-md px-8", + icon: "h-10 w-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const Button = React.forwardRef( + ({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button" + return ( + + ) + } +) +Button.displayName = "Button" + +export { Button, buttonVariants } diff --git a/client-next/components/ui/card.tsx b/client-next/components/ui/card.tsx new file mode 100644 index 0000000..afa13ec --- /dev/null +++ b/client-next/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/client-next/components/ui/checkbox.tsx b/client-next/components/ui/checkbox.tsx new file mode 100644 index 0000000..df61a13 --- /dev/null +++ b/client-next/components/ui/checkbox.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as CheckboxPrimitive from "@radix-ui/react-checkbox" +import { Check } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)) +Checkbox.displayName = CheckboxPrimitive.Root.displayName + +export { Checkbox } diff --git a/client-next/components/ui/dialog.tsx b/client-next/components/ui/dialog.tsx new file mode 100644 index 0000000..47ce215 --- /dev/null +++ b/client-next/components/ui/dialog.tsx @@ -0,0 +1,119 @@ +"use client" + +import * as React from "react" +import * as DialogPrimitive from "@radix-ui/react-dialog" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Dialog = DialogPrimitive.Root + +const DialogTrigger = DialogPrimitive.Trigger + +const DialogPortal = DialogPrimitive.Portal + +const DialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName + +const DialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)) +DialogContent.displayName = DialogPrimitive.Content.displayName + +const DialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogHeader.displayName = "DialogHeader" + +const DialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +DialogFooter.displayName = "DialogFooter" + +const DialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogTitle.displayName = DialogPrimitive.Title.displayName + +const DialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +DialogDescription.displayName = DialogPrimitive.Description.displayName + +export { + Dialog, + DialogPortal, + DialogOverlay, + DialogTrigger, + DialogContent, + DialogHeader, + DialogFooter, + DialogTitle, + DialogDescription, +} diff --git a/client-next/components/ui/input.tsx b/client-next/components/ui/input.tsx new file mode 100644 index 0000000..677d05f --- /dev/null +++ b/client-next/components/ui/input.tsx @@ -0,0 +1,25 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +export interface InputProps + extends React.InputHTMLAttributes {} + +const Input = React.forwardRef( + ({ className, type, ...props }, ref) => { + return ( + + ) + } +) +Input.displayName = "Input" + +export { Input } diff --git a/client-next/components/ui/label.tsx b/client-next/components/ui/label.tsx new file mode 100644 index 0000000..5341821 --- /dev/null +++ b/client-next/components/ui/label.tsx @@ -0,0 +1,26 @@ +"use client" + +import * as React from "react" +import * as LabelPrimitive from "@radix-ui/react-label" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const labelVariants = cva( + "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" +) + +const Label = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, ...props }, ref) => ( + +)) +Label.displayName = LabelPrimitive.Root.displayName + +export { Label } diff --git a/client-next/components/ui/select.tsx b/client-next/components/ui/select.tsx new file mode 100644 index 0000000..dabb6e4 --- /dev/null +++ b/client-next/components/ui/select.tsx @@ -0,0 +1,121 @@ +"use client" + +import * as React from "react" +import * as SelectPrimitive from "@radix-ui/react-select" +import { Check, ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Select = SelectPrimitive.Root + +const SelectGroup = SelectPrimitive.Group + +const SelectValue = SelectPrimitive.Value + +const SelectTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + {children} + + + + +)) +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName + +const SelectContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, position = "popper", ...props }, ref) => ( + + + + {children} + + + +)) +SelectContent.displayName = SelectPrimitive.Content.displayName + +const SelectLabel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectLabel.displayName = SelectPrimitive.Label.displayName + +const SelectItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)) +SelectItem.displayName = SelectPrimitive.Item.displayName + +const SelectSeparator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +SelectSeparator.displayName = SelectPrimitive.Separator.displayName + +export { + Select, + SelectGroup, + SelectValue, + SelectTrigger, + SelectContent, + SelectLabel, + SelectItem, + SelectSeparator, +} diff --git a/client-next/components/ui/separator.tsx b/client-next/components/ui/separator.tsx new file mode 100644 index 0000000..12d81c4 --- /dev/null +++ b/client-next/components/ui/separator.tsx @@ -0,0 +1,31 @@ +"use client" + +import * as React from "react" +import * as SeparatorPrimitive from "@radix-ui/react-separator" + +import { cn } from "@/lib/utils" + +const Separator = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>( + ( + { className, orientation = "horizontal", decorative = true, ...props }, + ref + ) => ( + + ) +) +Separator.displayName = SeparatorPrimitive.Root.displayName + +export { Separator } diff --git a/client-next/components/ui/skeleton.tsx b/client-next/components/ui/skeleton.tsx new file mode 100644 index 0000000..01b8b6d --- /dev/null +++ b/client-next/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/client-next/components/ui/tabs.tsx b/client-next/components/ui/tabs.tsx new file mode 100644 index 0000000..26eb109 --- /dev/null +++ b/client-next/components/ui/tabs.tsx @@ -0,0 +1,55 @@ +"use client" + +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/client-next/components/ui/toast.tsx b/client-next/components/ui/toast.tsx new file mode 100644 index 0000000..89a4980 --- /dev/null +++ b/client-next/components/ui/toast.tsx @@ -0,0 +1,78 @@ +import * as React from "react"; +import * as ToastPrimitives from "@radix-ui/react-toast"; +import { cva, type VariantProps } from "class-variance-authority"; +import { X } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const ToastProvider = ToastPrimitives.Provider; + +const ToastViewport = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +ToastViewport.displayName = ToastPrimitives.Viewport.displayName; + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +const Toast = React.forwardRef, React.ComponentPropsWithoutRef & VariantProps>( + ({ className, variant, ...props }, ref) => { + return ; + } +); +Toast.displayName = ToastPrimitives.Root.displayName; + +const ToastAction = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; + +const ToastClose = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + + + +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; + +const ToastTitle = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; + +const ToastDescription = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +ToastDescription.displayName = ToastPrimitives.Description.displayName; + +type ToastProps = React.ComponentPropsWithoutRef; + +type ToastActionElement = React.ReactElement; + +export { type ToastProps, type ToastActionElement, ToastProvider, ToastViewport, Toast, ToastTitle, ToastDescription, ToastClose, ToastAction }; diff --git a/client-next/components/ui/toaster.tsx b/client-next/components/ui/toaster.tsx new file mode 100644 index 0000000..e223385 --- /dev/null +++ b/client-next/components/ui/toaster.tsx @@ -0,0 +1,35 @@ +"use client" + +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" +import { useToast } from "@/components/ui/use-toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} diff --git a/client-next/components/ui/tooltip.tsx b/client-next/components/ui/tooltip.tsx new file mode 100644 index 0000000..30fc44d --- /dev/null +++ b/client-next/components/ui/tooltip.tsx @@ -0,0 +1,30 @@ +"use client" + +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/client-next/components/ui/use-toast.ts b/client-next/components/ui/use-toast.ts new file mode 100644 index 0000000..90d8959 --- /dev/null +++ b/client-next/components/ui/use-toast.ts @@ -0,0 +1,192 @@ +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_VALUE + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } diff --git a/client-next/constants/index.ts b/client-next/constants/index.ts new file mode 100644 index 0000000..02dc4fc --- /dev/null +++ b/client-next/constants/index.ts @@ -0,0 +1,206 @@ +import { Steps, Footer, FAQ, Commit } from "@/interfaces"; +import { + User, + Home, + Shirt, + Hammer, + Paperclip, + Scissors, + Recycle, + Gift, + Cpu, + Mountain, + Palette, + Camera, + Car, + Leaf, + Flower, + Blocks, + Gem, + ShowerHead, + ChefHat, + PawPrint, + Sofa, + Music, + CakeSlice, + Map, + Torus, + Asterisk, + Ruler, + PencilRuler, + Pencil, +} from "lucide-react"; + +export const categories: string[] = [ + "Anything", + "Home Decor", + "Fashion", + "Garden Projects", + "Kids Crafts", + "Jewelry Making", + "Woodworking", + "Paper Crafts", + "Pottery & Clay Projects", + "Textile & Sewing", + "Upcycling", + "Holiday Crafts", + "Tech DIYs", + "Beauty & Personal Care", + "Outdoor Projects", + "Kitchen Crafts", + "Pet Crafts", + "Furniture Makeovers", + "Art Projects", + "Photography DIYs", + "Musical Instruments", + "Car & Mechanical Crafts", + "Eco-friendly Crafts", + "Baking & Food Crafts", + "Travel & Adventure DIYs", +]; + +export const difficulties: { level: string; icon: React.ElementType }[] = [ + { level: "all", icon: Asterisk }, + { level: "beginner", icon: Pencil }, + { level: "intermediate", icon: Ruler }, + { level: "advanced", icon: PencilRuler }, +]; + +export const categoryIcons: { [key: string]: React.ElementType } = { + Anything: User, + "Home Decor": Home, + Fashion: Shirt, + "Garden Projects": Flower, + "Kids Crafts": Blocks, + "Jewelry Making": Gem, + Woodworking: Hammer, + "Paper Crafts": Paperclip, + "Pottery & Clay Projects": Torus, + "Textile & Sewing": Scissors, + Upcycling: Recycle, + "Holiday Crafts": Gift, + "Tech DIYs": Cpu, + "Beauty & Personal Care": ShowerHead, + "Outdoor Projects": Mountain, + "Kitchen Crafts": ChefHat, + "Pet Crafts": PawPrint, + "Furniture Makeovers": Sofa, + "Art Projects": Palette, + "Photography DIYs": Camera, + "Musical Instruments": Music, + "Car & Mechanical Crafts": Car, + "Eco-friendly Crafts": Leaf, + "Baking & Food Crafts": CakeSlice, + "Travel & Adventure DIYs": Map, +}; + +export const steps: Steps[] = [ + { title: "Materials", description: "List your available materials" }, + { title: "Generate", description: "Create project ideas" }, + { title: "View", description: "See the generated projects" }, +]; + +export const loadingMessages: string[] = ["Generating project ideas...", "Brewing some creativity...", "Almost there, hang tight...", "Fetching some inspiration..."]; + +export const footerData: Footer[] = [ + { + label: "Explore", + hash: "#", + links: [ + { label: "Generate DIY Project Idea", path: "/" }, + { label: "How-to Guides", path: "/guide" }, + ], + }, + { + label: "FAQ", + path: "/faq", + hash: "#getting-started", + links: [ + { label: "Getting Started", path: "/faq", hash: "#getting-started" }, + { label: "Usage Guidelines", path: "/faq", hash: "#usage-guidelines" }, + { + label: "Security and Privacy", + path: "/faq", + hash: "#security-privacy", + }, + { label: "Feedback and Suggestions", path: "/faq", hash: "#feedback" }, + ], + }, + { + label: "Social", + hash: "#", + links: [ + { label: "Email", href: "https://mail.google.com/" }, + { label: "Twitter", href: "https://twitter.com/" }, + { label: "Linkedin", href: "https://www.linkedin.com/" }, + ], + }, +]; + +export const faqs: FAQ[] = [ + { + id: "getting-started", + question: "Getting Started", + answer: + "It's straightforward! Visit the {homeLink}, input your preferences, such as materials and other options, and we'll generate a unique DIY project suggestion tailored for you. No sign-up required!", + links: { + homeLink: { href: "/", text: "MakeMeDIYspire homepage" }, + }, + }, + { + id: "makemediyspire", + question: "What is MakeMeDIYspire?", + answer: + "MakeMeDIYspire is a revolutionary platform powered by OpenAI. It provides unique DIY (Do It Yourself) project suggestions tailored for enthusiasts of all levels. Our platform not only offers project ideas but also outlines the materials required and the steps to bring these projects to life. Whether you're a seasoned DIYer or just starting, MakeMeDIYspire has something for you!", + }, + { + id: "how", + question: "How does MakeMeDIYspire work?", + answer: + "Utilizing the capabilities of OpenAI, our platform generates distinct DIY project ideas based on a variety of factors and categories. Once you select a project, you will receive a detailed list of materials needed and a step-by-step guide to complete the project. Refer to {guideLink} guide.", + links: { + guideLink: { href: "/how-to-guide", text: "How to use the MakeMeDIYspire DIY Idea Generator" }, + }, + }, + { + id: "usage-guidelines", + question: "Usage Guidelines", + answer: "MakeMeDIYspire is entirely free to use. Dive in, explore the multitude of ideas, and get started on your DIY journey!", + }, + { + id: "security-privacy", + question: "Security and Privacy", + answer: + "MakeMeDIYspire is a platform solely designed to generate DIY project ideas. We don't store or collect any personal information from our users. Just visit, generate ideas, and rest assured about your online privacy.", + }, + { + id: "feedback", + question: "How can I give feedback or suggestions?", + answer: "We're always eager to hear from our users! If you have any feedback or suggestions for MakeMeDIYspire, please {githubLink}. Your insights can help us enhance the platform for everyone.", + links: { + githubLink: { href: "https://github.com/Kevin-Umali/make-me/issues", text: "open a pull request or issue on our GitHub repository" }, + }, + }, +]; + +export const commits: Record = { + "2023": [ + { + date: "Sep 14, 2023", + summary: + "On Sep 14, 2023, multiple important changes and optimizations were made to the project. These changes included removing the 'robots.txt' file and updating the routing code, optimizing the build process through manual chunking, removing metadata for robots, and optimizing the home page. Additionally, there was a fix for the SEO score in Lighthouse. The most significant improvement involved enhancing SEO, adding FAQ and How-to guides, and improving the project's overall structure.", + operations: ["code-updates", "performance-enhancements", "code-updates", "bug-fixes", "ui-ux-improvements"], + }, + { + date: "Sep 13, 2023", + summary: + "On Sep 13, 2023, multiple enhancements were made. Origin whitelisting, fine-tuning, meta data updates, and user experience improvements took place. The backend underwent an API URL correction, removed a package-lock, and addressed deployment issues. Concurrency improvements were also made. The overall design was updated with new headers and footers.", + operations: ["code-updates", "bug-fixes", "ui-ux-improvements", "performance-enhancements"], + }, + { + date: "Sep 12, 2023", + summary: "On Sep 12, 2023, foundational setup took place with the creation of README and environment configuration files.", + operations: ["documentation"], + }, + ], +}; diff --git a/client-next/hooks/useInterval.ts b/client-next/hooks/useInterval.ts new file mode 100644 index 0000000..881fad2 --- /dev/null +++ b/client-next/hooks/useInterval.ts @@ -0,0 +1,22 @@ +import { useEffect, useRef } from "react"; + +function useInterval(callback: () => void, delay: number | null) { + const savedCallback = useRef<() => void>(() => {}); + + useEffect(() => { + savedCallback.current = callback; + }, [callback]); + + useEffect(() => { + function tick() { + savedCallback.current(); + } + + if (delay !== null) { + let id = setInterval(tick, delay); + return () => clearInterval(id); + } + }, [delay]); +} + +export default useInterval; diff --git a/client-next/interfaces/index.ts b/client-next/interfaces/index.ts new file mode 100644 index 0000000..28ac340 --- /dev/null +++ b/client-next/interfaces/index.ts @@ -0,0 +1,100 @@ +export interface FAQ { + id: string; + question: string; + answer?: string; + answerType?: string; + links?: { + [key: string]: FAQLinkData; + }; +} + +export interface FAQLinkData { + href: string; + text: string; +} + +export interface Footer { + label: string; + path?: string; + hash?: string; + href?: string; + links: FooterLink[]; +} + +export interface FooterLink { + label: string; + path?: string; + hash?: string; + href?: string; +} + +export interface Steps { + title: string; + description: string; +} + +export type Operation = "code-updates" | "bug-fixes" | "feature-additions" | "performance-enhancements" | "documentation" | "security-updates" | "ui-ux-improvements"; + +export interface Commit { + date: string; + summary: string; + operations: Operation[]; +} + +export interface HowToGuide { + id: number; + path: string; + metadataId: number; + content: string; + metadata: Metadata; +} + +export interface Metadata { + id: number; + title: string; + description: string; + imageUrl?: string | null; +} + +export interface ProjectLocationState { + title: string; + materials: string[]; + tools: string[]; + time: string; + budget: string; + tags: string[]; + description: string; +} + +export interface RelatedImages { + id: string; + width: number; + height: number; + color: string; + alt_description: string; + urls: Urls; + links: Links; + user: User; +} + +export interface Urls { + raw: string; + full: string; + regular: string; + small: string; + thumb: string; + small_s3: string; +} + +export interface Links { + self: string; + html: string; + download: string; + download_location: string; +} + +export interface User { + username: string; + name: string; + link: string; +} diff --git a/client-next/lib/index.ts b/client-next/lib/index.ts new file mode 100644 index 0000000..845d828 --- /dev/null +++ b/client-next/lib/index.ts @@ -0,0 +1,93 @@ +const API_URL = process.env.NEXT_PUBLIC_API_URL; + +type FetchApiOptions = { + method?: string; + body?: object; +}; + +const fetchApi = async (endpoint: string, { method = "GET", body }: FetchApiOptions = {}): Promise => { + try { + const options = { + method, + headers: { + "Content-Type": "application/json", + }, + body: body ? JSON.stringify(body) : undefined, + }; + + const response = await fetch(`${API_URL}${endpoint}`, options); + + if (!response.ok) { + const errorMessage = await response.text(); + throw new Error(errorMessage || "Network response was not ok"); + } + + return await response.json(); + } catch (error: any) { + console.error("Fetch API Error: ", error.message); + throw error; + } +}; + +export const generateProjectIdeas = async ( + materials: string[], + onlySpecified: boolean, + difficulty: string, + category: string, + tools: string[], + timeValue: number, + timeUnit: string | null, + budget: number, + endPurpose: string +): Promise => { + const time = timeValue && timeUnit ? `${timeValue} ${timeUnit}` : ""; + return fetchApi("/v1/generate/idea", { + method: "POST", + body: { materials, onlySpecified, difficulty, category, tools, time, budget, endPurpose }, + }); +}; + +export const generateProjectExplanations = async (title: string, materials: string[], tools: string[], time: string, budget: string, description: string): Promise => { + return fetchApi("/v1/generate/explain", { + method: "POST", + body: { title, materials, tools, time, budget, description }, + }); +}; + +export const searchImages = async (query: string): Promise => { + return fetchApi(`/v1/image/search?query=${encodeURIComponent(query)}`); +}; + +export const getGuideByPath = async (path: string): Promise => { + return fetchApi(`/v1/guide/${path}`); +}; + +export const getAllGuides = async (): Promise => { + return fetchApi("/v1/guide"); +}; + +export const saveShareLinkData = async (projectDetails: object, projectImage: object, explanation: string): Promise => { + return fetchApi("/v1/share", { + method: "POST", + body: { projectDetails, projectImage, explanation }, + }); +}; + +export const getShareLinkData = async (id: string): Promise => { + return fetchApi(`/v1/share/${id}`); +}; + +export const getTotalCountOfGeneratedIdea = async (): Promise => { + return fetchApi("/v1/counter"); +}; + +export const incrementCounterOfGeneratedIdea = async (): Promise => { + return fetchApi("/v1/counter", { method: "POST" }); +}; + +export const cleanMarkdown = (content: string) => { + return content + .split("\n") + .map((line) => line.trimStart()) + .join("\n"); +}; diff --git a/client-next/lib/utils.ts b/client-next/lib/utils.ts new file mode 100644 index 0000000..365058c --- /dev/null +++ b/client-next/lib/utils.ts @@ -0,0 +1,6 @@ +import { type ClassValue, clsx } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/client-next/next.config.js b/client-next/next.config.js new file mode 100644 index 0000000..767719f --- /dev/null +++ b/client-next/next.config.js @@ -0,0 +1,4 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = {} + +module.exports = nextConfig diff --git a/client-next/package.json b/client-next/package.json new file mode 100644 index 0000000..7cf79fb --- /dev/null +++ b/client-next/package.json @@ -0,0 +1,47 @@ +{ + "name": "client-next", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev -p 8080", + "build": "next build", + "start": "next start", + "lint": "next lint" + }, + "dependencies": { + "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-toast": "^1.1.5", + "@radix-ui/react-tooltip": "^1.0.7", + "@tailwindcss/typography": "^0.5.10", + "class-variance-authority": "^0.7.0", + "clsx": "^2.0.0", + "lucide-react": "^0.284.0", + "markdown-to-jsx": "^7.3.2", + "next": "13.5.4", + "next-themes": "^0.2.1", + "react": "^18", + "react-dom": "^18", + "react-markdown": "^9.0.0", + "tailwind-merge": "^1.14.0", + "tailwindcss-animate": "^1.0.7" + }, + "devDependencies": { + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "autoprefixer": "^10", + "eslint": "^8", + "eslint-config-next": "13.5.4", + "postcss": "^8", + "tailwindcss": "^3", + "typescript": "^5" + } +} diff --git a/client-next/postcss.config.js b/client-next/postcss.config.js new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/client-next/postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/client-next/public/android-chrome-192x192.png b/client-next/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..5e8449ef886b8c876554843249eed790bd203145 GIT binary patch literal 10911 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 z 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 B6p#P_ literal 0 HcmV?d00001 diff --git a/client-next/public/android-chrome-512x512.png b/client-next/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..d46e3fd2a0dfadab14ba90b109489edc2f78facc GIT binary patch literal 28096 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 literal 0 HcmV?d00001 diff --git a/client-next/public/favicon-16x16.png b/client-next/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..fe8f0e239d848e7b65e3615f61be8ba95cf34f37 GIT binary patch literal 482 zcmV<80UiE{P)Zlp&-~<2#3WX`2iuI z%>}{h#mXYy6`Hq83`b%@j0?l=dpmDRlb0)GCc%Y#0$KowbqD7GjurojZ()Lff z>^P@P;FfUpl@*Ofk!=1v)M#0moroEn1Ct>LLe9nVzpX9(=<--`~66z zQt0>lkYyQ}Oa@M;6IQDg^Z6VWiv^3t;*|;L>2%s&sZ`+e`7oVM(P%U{d9&G|*Xv<2 znLtq#wA*deYBhAbT`ZSNv|25L3ls_kyRPdf7K<1Thwykjh{xloR;!3aBCy$P2!%oz zjYdc$67YJx7z_re*XvwBjm2UC!!STI#^W*EZZ}*m7cZX8X6SS}5Ji#a^Z7i&;V??2 z66=H?VW)|^tWPGBD3{CJ6V@FKZsK0o$jn=>*R1&S8d-4ALt+94Kjp2+zdw`Nv%S9a YJH6G6UNLL?@c;k-07*qoM6N<$f~!5%I{*Lx literal 0 HcmV?d00001 diff --git a/client-next/public/favicon-32x32.png b/client-next/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..8b8575ef751137f3e283ffdc813a6b7f5901389e GIT binary patch literal 1150 zcmV-^1cCdBP)UvO>xVmv=#K*qf+&g`8b9I+CW?ys>4%_* zBJN6zj)J4&xQy$7E5Ut1bF1(@Ivu;G6^AZ3ba&mlRp(aKt$t4d?d5x6`inq-fV034 z0;D7PN8q!-2Y~^B?g!mhy(_+I0oV(h_X1G@Mq1dD$%R0NK&ikdO?xyEpkRdpnF0n{ z239q@5sKdhavwR^2u)6u%0rs4hH4fa=>T@alzvOK$eXm!Acw zPh?RgKqnvn2Emm{yj9MGG6AajUoV(;bqS<$ypeiBnLve5X1#FQ)w%mkEWD#di2z-- zj{?u__NVfu+wr5oCyBtH;zz<$y=bymC=|HAzt?2aC>9fm08O(m0@@B4cIxWt3O_$T z7#J8}dU_iD{rz%4(k(77A}%ftqobqhNYJo)O$2DdwQGBNOG`_%wY9<7*%>1vBe=P_ zL0DKACMPHP^Wx$HwzjrdSy@4PdOB)rYc;u!CISrtN!k*yx3`Ctl@&ZZJWy3tg{!M8 z1O^6j26A$8P*PF?Jv}|d#>S$fqk{=*kUxmP-;_}@(%08VK|ulP>go_29E^d10c>n+ zu*18%JK2weg9GN~=3r)K#wR8@JUqnw{5*VpeUXrmfbsEhEG;b|CME{k+uIl#8bVM| z5R8nB(ACu?`I;sI2LhI)iHwY7)>~U!pj5A~uQP$r&`?xVR3JM$n>SWgRw5uE08UO$ zGAY_aBuKxzyNmq%e3X`!;^gE6-QC@Yh=@Q>PYNhCDD68W0+gfgNYmcl&W7&p z?kFoOgTKE&5)%`VmzRgt)m2V42@&h#l?lmQ0nPgQt+&-EKE#HU~_X5J3Bi}KrX?|%nUOQ z2?@d4+8TFuN=gc+k%Ig9_`uN6keiT#4-O8pj>r&sDPuJ?HOR=wKzVsNOiWCWnVHF% z_4M@QPb!a_n;Xv0&$$G06Vk~pFE5$E!omVKT6A49QnuAkTsF%FGy|KT)kNEg_E};-^Lirj}Eh*hJ!KewT zsU8z!V`C;jr#n48ebh$k4H`ytavECHoXN?_oY@EAYxtx=q&C_k3r$T;FgG{nfW^hd z@bdEF>!~C>h@LfywIM>Yq@kgK`-Ym4dPPa(QB1lQ|1**T(QqSNUtg<7ZZtuPG8NG@2t*VPE`lTH+en(7ryGhocAZ~ QrT_o{07*qoM6N<$f(U*Z4*&oF literal 0 HcmV?d00001 diff --git a/client-next/tailwind.config.ts b/client-next/tailwind.config.ts new file mode 100644 index 0000000..50885a5 --- /dev/null +++ b/client-next/tailwind.config.ts @@ -0,0 +1,71 @@ +/** @type {import('tailwindcss').Config} */ +module.exports = { + darkMode: ["class"], + content: ["./pages/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", "./app/**/*.{js,ts,jsx,tsx,mdx}", "./src/**/*.{js,ts,jsx,tsx,mdx}"], + theme: { + container: { + center: true, + padding: "2rem", + screens: { + "2xl": "1400px", + }, + }, + extend: { + colors: { + border: "hsl(var(--border))", + input: "hsl(var(--input))", + ring: "hsl(var(--ring))", + background: "hsl(var(--background))", + foreground: "hsl(var(--foreground))", + primary: { + DEFAULT: "hsl(var(--primary))", + foreground: "hsl(var(--primary-foreground))", + }, + secondary: { + DEFAULT: "hsl(var(--secondary))", + foreground: "hsl(var(--secondary-foreground))", + }, + destructive: { + DEFAULT: "hsl(var(--destructive))", + foreground: "hsl(var(--destructive-foreground))", + }, + muted: { + DEFAULT: "hsl(var(--muted))", + foreground: "hsl(var(--muted-foreground))", + }, + accent: { + DEFAULT: "hsl(var(--accent))", + foreground: "hsl(var(--accent-foreground))", + }, + popover: { + DEFAULT: "hsl(var(--popover))", + foreground: "hsl(var(--popover-foreground))", + }, + card: { + DEFAULT: "hsl(var(--card))", + foreground: "hsl(var(--card-foreground))", + }, + }, + borderRadius: { + lg: "var(--radius)", + md: "calc(var(--radius) - 2px)", + sm: "calc(var(--radius) - 4px)", + }, + keyframes: { + "accordion-down": { + from: { height: 0 }, + to: { height: "var(--radix-accordion-content-height)" }, + }, + "accordion-up": { + from: { height: "var(--radix-accordion-content-height)" }, + to: { height: 0 }, + }, + }, + animation: { + "accordion-down": "accordion-down 0.2s ease-out", + "accordion-up": "accordion-up 0.2s ease-out", + }, + }, + }, + plugins: [require("tailwindcss-animate"), require("@tailwindcss/typography")], +}; diff --git a/client-next/tsconfig.json b/client-next/tsconfig.json new file mode 100644 index 0000000..c714696 --- /dev/null +++ b/client-next/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "es5", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./*"] + } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} From 870c25c025a323587fe59ef4151037ba74e160a2 Mon Sep 17 00:00:00 2001 From: Kevin-Umali Date: Fri, 6 Oct 2023 23:58:40 +0800 Subject: [PATCH 2/4] Test vercel deploy --- client-next/app/guide/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client-next/app/guide/page.tsx b/client-next/app/guide/page.tsx index 6c0252e..7fd0ee9 100644 --- a/client-next/app/guide/page.tsx +++ b/client-next/app/guide/page.tsx @@ -4,7 +4,7 @@ import { Metadata } from "next"; export const metadata: Metadata = { title: "MakeMeDIYspire How-to Guides", description: "Navigate through our comprehensive guides and maximize the potential of the MakeMeDIYspire DIY project generator.", - keywords: ["DIY Project Guides", "MakeMeDIYspire Instructions", "DIY Project Tips", "DIY Crafting Guide", "DIY Project Creation", "DIY Generator Guide", "DIY Inspiration"], + keywords: ["DIY Project Guides", "MakeMeDIYspire Instructions", "DIY Project Tips", "DIY Crafting Guide", "DIY Project Creation", "DIY Generator Guide", "DIY Inspiration", "AI DIY"], metadataBase: new URL("https://www.diyspire/guide"), applicationName: "MakeMeDIYspire", }; From 3b19398d2ef722f0d97c7f66deeebac03eeae5a0 Mon Sep 17 00:00:00 2001 From: Kevin-Umali Date: Sat, 7 Oct 2023 13:01:58 +0800 Subject: [PATCH 3/4] changed to nextjs --- client-next/.eslintrc.json | 3 - client-next/.gitignore | 35 --- client-next/README.md | 36 --- client-next/components/footer.tsx | 46 ---- .../components/generate/generate-loading.tsx | 21 -- client-next/components/ui/accordion.tsx | 60 ----- client-next/components/ui/alert-dialog.tsx | 141 ------------ client-next/components/ui/alert.tsx | 59 ----- client-next/components/ui/badge.tsx | 36 --- client-next/components/ui/card.tsx | 79 ------- client-next/components/ui/checkbox.tsx | 30 --- client-next/components/ui/dialog.tsx | 119 ---------- client-next/components/ui/input.tsx | 25 -- client-next/components/ui/label.tsx | 26 --- client-next/components/ui/select.tsx | 121 ---------- client-next/components/ui/separator.tsx | 31 --- client-next/components/ui/skeleton.tsx | 15 -- client-next/components/ui/tabs.tsx | 55 ----- client-next/components/ui/tooltip.tsx | 30 --- client-next/next.config.js | 4 - client-next/package.json | 47 ---- client-next/tsconfig.json | 27 --- client/.env.example | 6 +- client/.eslintrc.cjs | 32 --- client/.eslintrc.json | 36 +++ client/.gitignore | 59 ++--- client/.lintstagedrc.json | 2 +- client/README.md | 54 ++--- {client-next => client}/app/faq/faq.tsx | 2 +- {client-next => client}/app/faq/page.tsx | 0 {client-next => client}/app/globals.css | 0 .../app/guide/[guide_name]/guide-name.tsx | 2 +- .../app/guide/[guide_name]/page.tsx | 4 +- {client-next => client}/app/guide/guide.tsx | 0 {client-next => client}/app/guide/page.tsx | 0 {client-next => client}/app/icon.ico | Bin {client-next => client}/app/layout.tsx | 0 {client-next => client}/app/page.tsx | 64 +++--- client/app/project-detail/[id]/page.tsx | 19 ++ .../[id]/project-detail-by-id.tsx | 84 +++++++ client/app/project-detail/page.tsx | 14 ++ client/app/project-detail/project-detail.tsx | 121 ++++++++++ {client-next => client}/app/robots.ts | 0 {client-next => client}/components.json | 2 +- .../components/custom-markdown.tsx | 0 client/components/footer.tsx | 49 ++++ .../components/generate/budget-filter.tsx | 14 +- .../components/generate/category-filter.tsx | 5 +- .../components/generate/difficulty-filter.tsx | 9 +- .../components/generate/generate-loading.tsx | 34 +++ .../components/generate/material-input.tsx | 6 +- .../components/generate/project-tabs.tsx | 16 +- .../components/generate/purpose-filter.tsx | 0 .../components/generate/safety-check.tsx | 2 +- .../generate/time-availability-filter.tsx | 0 .../generate/tools-available-input.tsx | 4 +- {client-next => client}/components/navbar.tsx | 4 +- .../project-detail/project-image.tsx | 92 ++++++++ .../project-detail/project-info.tsx | 62 +++++ .../project-detail/project-step.tsx | 43 ++++ .../project-detail/share-dialog.tsx | 92 ++++++++ .../components/theme-provider.tsx | 0 client/components/ui/accordion.tsx | 45 ++++ client/components/ui/alert-dialog.tsx | 86 +++++++ client/components/ui/alert.tsx | 33 +++ client/components/ui/badge.tsx | 26 +++ .../components/ui/button.tsx | 47 ++-- client/components/ui/card.tsx | 33 +++ client/components/ui/checkbox.tsx | 25 ++ client/components/ui/dialog.tsx | 64 ++++++ client/components/ui/input.tsx | 22 ++ client/components/ui/label.tsx | 16 ++ client/components/ui/select.tsx | 84 +++++++ client/components/ui/separator.tsx | 21 ++ client/components/ui/skeleton.tsx | 7 + client/components/ui/tabs.tsx | 36 +++ .../components/ui/toast.tsx | 8 +- .../components/ui/toaster.tsx | 23 +- client/components/ui/tooltip.tsx | 29 +++ .../components/ui/use-toast.ts | 131 +++++------ {client-next => client}/constants/index.ts | 0 client/hooks/useClipboard.ts | 19 ++ {client-next => client}/hooks/useInterval.ts | 2 +- client/index.html | 30 --- {client-next => client}/interfaces/index.ts | 0 {client-next => client}/lib/index.ts | 6 +- {client-next => client}/lib/utils.ts | 0 client/next.config.js | 8 + client/package.json | 79 ++++--- {client-next => client}/postcss.config.js | 2 +- .../public/android-chrome-192x192.png | Bin .../public/android-chrome-512x512.png | Bin .../public/apple-touch-icon.png | Bin .../public/favicon-16x16.png | Bin .../public/favicon-32x32.png | Bin client/public/image/icon.svg | 117 ---------- client/public/robots.txt | 2 - client/src/App.tsx | 36 --- client/src/api/backend-api.ts | 68 ------ client/src/components/CustomMarkdown.tsx | 86 ------- client/src/components/ErrorFallback.tsx | 24 -- client/src/components/Footer.tsx | 52 ----- client/src/components/MetaTag.tsx | 54 ----- client/src/components/Navbar.tsx | 37 --- .../src/components/generate/BudgetFilter.tsx | 26 --- .../components/generate/CategoryFilter.tsx | 55 ----- .../components/generate/DifficultyFilter.tsx | 50 ---- .../components/generate/LoadingComponent.tsx | 23 -- .../src/components/generate/MaterialInput.tsx | 65 ------ .../src/components/generate/ProjectTabs.tsx | 78 ------- .../src/components/generate/PurposeFilter.tsx | 52 ----- .../src/components/generate/SafetyCheck.tsx | 15 -- .../generate/TimeAvailabilityFilter.tsx | 59 ----- .../generate/ToolsAvailableInput.tsx | 49 ---- client/src/components/generate/index.ts | 10 - .../project-details/ProjectImage.tsx | 84 ------- .../project-details/ProjectInfo.tsx | 82 ------- .../project-details/ProjectSteps.tsx | 40 ---- .../components/project-details/ShareModal.tsx | 87 ------- .../src/components/project-details/index.ts | 4 - client/src/constants/index.ts | 195 ---------------- client/src/hooks/useInterval.ts | 21 -- client/src/main.tsx | 17 -- client/src/pages/FAQ.tsx | 85 ------- client/src/pages/Home.tsx | 217 ------------------ client/src/pages/HowToGuideDetail.tsx | 58 ----- client/src/pages/HowToGuideList.tsx | 62 ----- client/src/pages/ProjectDetail.tsx | 136 ----------- client/src/pages/ProjectDetailById.tsx | 89 ------- client/src/styles/App.css | 7 - client/src/styles/index.css | 18 -- client/src/styles/theme.ts | 107 --------- client/src/types/index.ts | 92 -------- client/src/utils/index.ts | 65 ------ client/src/vite-env.d.ts | 1 - {client-next => client}/tailwind.config.ts | 0 client/tsconfig.json | 36 +-- client/tsconfig.node.json | 10 - client/vite.config.ts | 37 --- 139 files changed, 1492 insertions(+), 3877 deletions(-) delete mode 100644 client-next/.eslintrc.json delete mode 100644 client-next/.gitignore delete mode 100644 client-next/README.md delete mode 100644 client-next/components/footer.tsx delete mode 100644 client-next/components/generate/generate-loading.tsx delete mode 100644 client-next/components/ui/accordion.tsx delete mode 100644 client-next/components/ui/alert-dialog.tsx delete mode 100644 client-next/components/ui/alert.tsx delete mode 100644 client-next/components/ui/badge.tsx delete mode 100644 client-next/components/ui/card.tsx delete mode 100644 client-next/components/ui/checkbox.tsx delete mode 100644 client-next/components/ui/dialog.tsx delete mode 100644 client-next/components/ui/input.tsx delete mode 100644 client-next/components/ui/label.tsx delete mode 100644 client-next/components/ui/select.tsx delete mode 100644 client-next/components/ui/separator.tsx delete mode 100644 client-next/components/ui/skeleton.tsx delete mode 100644 client-next/components/ui/tabs.tsx delete mode 100644 client-next/components/ui/tooltip.tsx delete mode 100644 client-next/next.config.js delete mode 100644 client-next/package.json delete mode 100644 client-next/tsconfig.json delete mode 100644 client/.eslintrc.cjs create mode 100644 client/.eslintrc.json rename {client-next => client}/app/faq/faq.tsx (99%) rename {client-next => client}/app/faq/page.tsx (100%) rename {client-next => client}/app/globals.css (100%) rename {client-next => client}/app/guide/[guide_name]/guide-name.tsx (98%) rename {client-next => client}/app/guide/[guide_name]/page.tsx (85%) rename {client-next => client}/app/guide/guide.tsx (100%) rename {client-next => client}/app/guide/page.tsx (100%) rename {client-next => client}/app/icon.ico (100%) rename {client-next => client}/app/layout.tsx (100%) rename {client-next => client}/app/page.tsx (73%) create mode 100644 client/app/project-detail/[id]/page.tsx create mode 100644 client/app/project-detail/[id]/project-detail-by-id.tsx create mode 100644 client/app/project-detail/page.tsx create mode 100644 client/app/project-detail/project-detail.tsx rename {client-next => client}/app/robots.ts (100%) rename {client-next => client}/components.json (99%) rename {client-next => client}/components/custom-markdown.tsx (100%) create mode 100644 client/components/footer.tsx rename {client-next => client}/components/generate/budget-filter.tsx (60%) rename {client-next => client}/components/generate/category-filter.tsx (89%) rename {client-next => client}/components/generate/difficulty-filter.tsx (75%) create mode 100644 client/components/generate/generate-loading.tsx rename {client-next => client}/components/generate/material-input.tsx (95%) rename {client-next => client}/components/generate/project-tabs.tsx (84%) rename {client-next => client}/components/generate/purpose-filter.tsx (100%) rename {client-next => client}/components/generate/safety-check.tsx (85%) rename {client-next => client}/components/generate/time-availability-filter.tsx (100%) rename {client-next => client}/components/generate/tools-available-input.tsx (97%) rename {client-next => client}/components/navbar.tsx (84%) create mode 100644 client/components/project-detail/project-image.tsx create mode 100644 client/components/project-detail/project-info.tsx create mode 100644 client/components/project-detail/project-step.tsx create mode 100644 client/components/project-detail/share-dialog.tsx rename {client-next => client}/components/theme-provider.tsx (100%) create mode 100644 client/components/ui/accordion.tsx create mode 100644 client/components/ui/alert-dialog.tsx create mode 100644 client/components/ui/alert.tsx create mode 100644 client/components/ui/badge.tsx rename {client-next => client}/components/ui/button.tsx (50%) create mode 100644 client/components/ui/card.tsx create mode 100644 client/components/ui/checkbox.tsx create mode 100644 client/components/ui/dialog.tsx create mode 100644 client/components/ui/input.tsx create mode 100644 client/components/ui/label.tsx create mode 100644 client/components/ui/select.tsx create mode 100644 client/components/ui/separator.tsx create mode 100644 client/components/ui/skeleton.tsx create mode 100644 client/components/ui/tabs.tsx rename {client-next => client}/components/ui/toast.tsx (99%) rename {client-next => client}/components/ui/toaster.tsx (54%) create mode 100644 client/components/ui/tooltip.tsx rename {client-next => client}/components/ui/use-toast.ts (57%) rename {client-next => client}/constants/index.ts (100%) create mode 100644 client/hooks/useClipboard.ts rename {client-next => client}/hooks/useInterval.ts (91%) delete mode 100644 client/index.html rename {client-next => client}/interfaces/index.ts (100%) rename {client-next => client}/lib/index.ts (90%) rename {client-next => client}/lib/utils.ts (100%) create mode 100644 client/next.config.js rename {client-next => client}/postcss.config.js (96%) rename {client-next => client}/public/android-chrome-192x192.png (100%) rename {client-next => client}/public/android-chrome-512x512.png (100%) rename {client-next => client}/public/apple-touch-icon.png (100%) rename {client-next => client}/public/favicon-16x16.png (100%) rename {client-next => client}/public/favicon-32x32.png (100%) delete mode 100644 client/public/image/icon.svg delete mode 100644 client/public/robots.txt delete mode 100644 client/src/App.tsx delete mode 100644 client/src/api/backend-api.ts delete mode 100644 client/src/components/CustomMarkdown.tsx delete mode 100644 client/src/components/ErrorFallback.tsx delete mode 100644 client/src/components/Footer.tsx delete mode 100644 client/src/components/MetaTag.tsx delete mode 100644 client/src/components/Navbar.tsx delete mode 100644 client/src/components/generate/BudgetFilter.tsx delete mode 100644 client/src/components/generate/CategoryFilter.tsx delete mode 100644 client/src/components/generate/DifficultyFilter.tsx delete mode 100644 client/src/components/generate/LoadingComponent.tsx delete mode 100644 client/src/components/generate/MaterialInput.tsx delete mode 100644 client/src/components/generate/ProjectTabs.tsx delete mode 100644 client/src/components/generate/PurposeFilter.tsx delete mode 100644 client/src/components/generate/SafetyCheck.tsx delete mode 100644 client/src/components/generate/TimeAvailabilityFilter.tsx delete mode 100644 client/src/components/generate/ToolsAvailableInput.tsx delete mode 100644 client/src/components/generate/index.ts delete mode 100644 client/src/components/project-details/ProjectImage.tsx delete mode 100644 client/src/components/project-details/ProjectInfo.tsx delete mode 100644 client/src/components/project-details/ProjectSteps.tsx delete mode 100644 client/src/components/project-details/ShareModal.tsx delete mode 100644 client/src/components/project-details/index.ts delete mode 100644 client/src/constants/index.ts delete mode 100644 client/src/hooks/useInterval.ts delete mode 100644 client/src/main.tsx delete mode 100644 client/src/pages/FAQ.tsx delete mode 100644 client/src/pages/Home.tsx delete mode 100644 client/src/pages/HowToGuideDetail.tsx delete mode 100644 client/src/pages/HowToGuideList.tsx delete mode 100644 client/src/pages/ProjectDetail.tsx delete mode 100644 client/src/pages/ProjectDetailById.tsx delete mode 100644 client/src/styles/App.css delete mode 100644 client/src/styles/index.css delete mode 100644 client/src/styles/theme.ts delete mode 100644 client/src/types/index.ts delete mode 100644 client/src/utils/index.ts delete mode 100644 client/src/vite-env.d.ts rename {client-next => client}/tailwind.config.ts (100%) delete mode 100644 client/tsconfig.node.json delete mode 100644 client/vite.config.ts diff --git a/client-next/.eslintrc.json b/client-next/.eslintrc.json deleted file mode 100644 index bffb357..0000000 --- a/client-next/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": "next/core-web-vitals" -} diff --git a/client-next/.gitignore b/client-next/.gitignore deleted file mode 100644 index 8f322f0..0000000 --- a/client-next/.gitignore +++ /dev/null @@ -1,35 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo -next-env.d.ts diff --git a/client-next/README.md b/client-next/README.md deleted file mode 100644 index c403366..0000000 --- a/client-next/README.md +++ /dev/null @@ -1,36 +0,0 @@ -This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). - -## Getting Started - -First, run the development server: - -```bash -npm run dev -# or -yarn dev -# or -pnpm dev -# or -bun dev -``` - -Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. - -You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. - -This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. - -## Learn More - -To learn more about Next.js, take a look at the following resources: - -- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. -- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. - -You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! - -## Deploy on Vercel - -The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/client-next/components/footer.tsx b/client-next/components/footer.tsx deleted file mode 100644 index 4b07422..0000000 --- a/client-next/components/footer.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import Link from "next/link"; -import { footerData } from "@/constants"; - -const Footer: React.FC = () => { - return ( -
-
-
- {footerData.map((data, index) => ( -
- - {data.label} - -
- {data.links.map((link, linkIndex) => ( - - {link.label} - - ))} -
-
- ))} -
- -
-

- Powered by MakeMeDIYspire ✨ | Made with ❤️ by -{" "} - - Kooma - -

-
-
-
- ); -}; - -export default Footer; diff --git a/client-next/components/generate/generate-loading.tsx b/client-next/components/generate/generate-loading.tsx deleted file mode 100644 index d696dab..0000000 --- a/client-next/components/generate/generate-loading.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useState } from "react"; -import { loadingMessages } from "@/constants"; -import useInterval from "@/hooks/useInterval"; -import { Loader } from "lucide-react"; - -const GenerateLoading: React.FC = () => { - const [currentMessageIndex, setCurrentMessageIndex] = useState(0); - - useInterval(() => { - setCurrentMessageIndex((prevIndex) => (prevIndex + 1) % loadingMessages.length); - }, 5000); - - return ( -
- -

{loadingMessages[currentMessageIndex]}

-
- ); -}; - -export default GenerateLoading; diff --git a/client-next/components/ui/accordion.tsx b/client-next/components/ui/accordion.tsx deleted file mode 100644 index 937620a..0000000 --- a/client-next/components/ui/accordion.tsx +++ /dev/null @@ -1,60 +0,0 @@ -"use client" - -import * as React from "react" -import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { ChevronDown } from "lucide-react" - -import { cn } from "@/lib/utils" - -const Accordion = AccordionPrimitive.Root - -const AccordionItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AccordionItem.displayName = "AccordionItem" - -const AccordionTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - svg]:rotate-180", - className - )} - {...props} - > - {children} - - - -)) -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName - -const AccordionContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - -
{children}
-
-)) -AccordionContent.displayName = AccordionPrimitive.Content.displayName - -export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/client-next/components/ui/alert-dialog.tsx b/client-next/components/ui/alert-dialog.tsx deleted file mode 100644 index c7925a0..0000000 --- a/client-next/components/ui/alert-dialog.tsx +++ /dev/null @@ -1,141 +0,0 @@ -"use client" - -import * as React from "react" -import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" - -import { cn } from "@/lib/utils" -import { buttonVariants } from "@/components/ui/button" - -const AlertDialog = AlertDialogPrimitive.Root - -const AlertDialogTrigger = AlertDialogPrimitive.Trigger - -const AlertDialogPortal = AlertDialogPrimitive.Portal - -const AlertDialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName - -const AlertDialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName - -const AlertDialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogHeader.displayName = "AlertDialogHeader" - -const AlertDialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogFooter.displayName = "AlertDialogFooter" - -const AlertDialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName - -const AlertDialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName - -const AlertDialogAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName - -const AlertDialogCancel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName - -export { - AlertDialog, - AlertDialogPortal, - AlertDialogOverlay, - AlertDialogTrigger, - AlertDialogContent, - AlertDialogHeader, - AlertDialogFooter, - AlertDialogTitle, - AlertDialogDescription, - AlertDialogAction, - AlertDialogCancel, -} diff --git a/client-next/components/ui/alert.tsx b/client-next/components/ui/alert.tsx deleted file mode 100644 index 41fa7e0..0000000 --- a/client-next/components/ui/alert.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const alertVariants = cva( - "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", - { - variants: { - variant: { - default: "bg-background text-foreground", - destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps ->(({ className, variant, ...props }, ref) => ( -
-)) -Alert.displayName = "Alert" - -const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertTitle.displayName = "AlertTitle" - -const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertDescription.displayName = "AlertDescription" - -export { Alert, AlertTitle, AlertDescription } diff --git a/client-next/components/ui/badge.tsx b/client-next/components/ui/badge.tsx deleted file mode 100644 index f000e3e..0000000 --- a/client-next/components/ui/badge.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import * as React from "react" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const badgeVariants = cva( - "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", - secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", - destructive: - "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", - outline: "text-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ) -} - -export { Badge, badgeVariants } diff --git a/client-next/components/ui/card.tsx b/client-next/components/ui/card.tsx deleted file mode 100644 index afa13ec..0000000 --- a/client-next/components/ui/card.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -const Card = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -Card.displayName = "Card" - -const CardHeader = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardHeader.displayName = "CardHeader" - -const CardTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardTitle.displayName = "CardTitle" - -const CardDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardDescription.displayName = "CardDescription" - -const CardContent = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -

-)) -CardContent.displayName = "CardContent" - -const CardFooter = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -CardFooter.displayName = "CardFooter" - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/client-next/components/ui/checkbox.tsx b/client-next/components/ui/checkbox.tsx deleted file mode 100644 index df61a13..0000000 --- a/client-next/components/ui/checkbox.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client" - -import * as React from "react" -import * as CheckboxPrimitive from "@radix-ui/react-checkbox" -import { Check } from "lucide-react" - -import { cn } from "@/lib/utils" - -const Checkbox = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - - - -)) -Checkbox.displayName = CheckboxPrimitive.Root.displayName - -export { Checkbox } diff --git a/client-next/components/ui/dialog.tsx b/client-next/components/ui/dialog.tsx deleted file mode 100644 index 47ce215..0000000 --- a/client-next/components/ui/dialog.tsx +++ /dev/null @@ -1,119 +0,0 @@ -"use client" - -import * as React from "react" -import * as DialogPrimitive from "@radix-ui/react-dialog" -import { X } from "lucide-react" - -import { cn } from "@/lib/utils" - -const Dialog = DialogPrimitive.Root - -const DialogTrigger = DialogPrimitive.Trigger - -const DialogPortal = DialogPrimitive.Portal - -const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName - -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - {children} - - - Close - - - -)) -DialogContent.displayName = DialogPrimitive.Content.displayName - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -DialogHeader.displayName = "DialogHeader" - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-) -DialogFooter.displayName = "DialogFooter" - -const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogTitle.displayName = DialogPrimitive.Title.displayName - -const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -DialogDescription.displayName = DialogPrimitive.Description.displayName - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -} diff --git a/client-next/components/ui/input.tsx b/client-next/components/ui/input.tsx deleted file mode 100644 index 677d05f..0000000 --- a/client-next/components/ui/input.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import * as React from "react" - -import { cn } from "@/lib/utils" - -export interface InputProps - extends React.InputHTMLAttributes {} - -const Input = React.forwardRef( - ({ className, type, ...props }, ref) => { - return ( - - ) - } -) -Input.displayName = "Input" - -export { Input } diff --git a/client-next/components/ui/label.tsx b/client-next/components/ui/label.tsx deleted file mode 100644 index 5341821..0000000 --- a/client-next/components/ui/label.tsx +++ /dev/null @@ -1,26 +0,0 @@ -"use client" - -import * as React from "react" -import * as LabelPrimitive from "@radix-ui/react-label" -import { cva, type VariantProps } from "class-variance-authority" - -import { cn } from "@/lib/utils" - -const labelVariants = cva( - "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" -) - -const Label = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, ...props }, ref) => ( - -)) -Label.displayName = LabelPrimitive.Root.displayName - -export { Label } diff --git a/client-next/components/ui/select.tsx b/client-next/components/ui/select.tsx deleted file mode 100644 index dabb6e4..0000000 --- a/client-next/components/ui/select.tsx +++ /dev/null @@ -1,121 +0,0 @@ -"use client" - -import * as React from "react" -import * as SelectPrimitive from "@radix-ui/react-select" -import { Check, ChevronDown } from "lucide-react" - -import { cn } from "@/lib/utils" - -const Select = SelectPrimitive.Root - -const SelectGroup = SelectPrimitive.Group - -const SelectValue = SelectPrimitive.Value - -const SelectTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - {children} - - - - -)) -SelectTrigger.displayName = SelectPrimitive.Trigger.displayName - -const SelectContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, position = "popper", ...props }, ref) => ( - - - - {children} - - - -)) -SelectContent.displayName = SelectPrimitive.Content.displayName - -const SelectLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -SelectLabel.displayName = SelectPrimitive.Label.displayName - -const SelectItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - - - - - - - {children} - -)) -SelectItem.displayName = SelectPrimitive.Item.displayName - -const SelectSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -SelectSeparator.displayName = SelectPrimitive.Separator.displayName - -export { - Select, - SelectGroup, - SelectValue, - SelectTrigger, - SelectContent, - SelectLabel, - SelectItem, - SelectSeparator, -} diff --git a/client-next/components/ui/separator.tsx b/client-next/components/ui/separator.tsx deleted file mode 100644 index 12d81c4..0000000 --- a/client-next/components/ui/separator.tsx +++ /dev/null @@ -1,31 +0,0 @@ -"use client" - -import * as React from "react" -import * as SeparatorPrimitive from "@radix-ui/react-separator" - -import { cn } from "@/lib/utils" - -const Separator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->( - ( - { className, orientation = "horizontal", decorative = true, ...props }, - ref - ) => ( - - ) -) -Separator.displayName = SeparatorPrimitive.Root.displayName - -export { Separator } diff --git a/client-next/components/ui/skeleton.tsx b/client-next/components/ui/skeleton.tsx deleted file mode 100644 index 01b8b6d..0000000 --- a/client-next/components/ui/skeleton.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { cn } from "@/lib/utils" - -function Skeleton({ - className, - ...props -}: React.HTMLAttributes) { - return ( -
- ) -} - -export { Skeleton } diff --git a/client-next/components/ui/tabs.tsx b/client-next/components/ui/tabs.tsx deleted file mode 100644 index 26eb109..0000000 --- a/client-next/components/ui/tabs.tsx +++ /dev/null @@ -1,55 +0,0 @@ -"use client" - -import * as React from "react" -import * as TabsPrimitive from "@radix-ui/react-tabs" - -import { cn } from "@/lib/utils" - -const Tabs = TabsPrimitive.Root - -const TabsList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsList.displayName = TabsPrimitive.List.displayName - -const TabsTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsTrigger.displayName = TabsPrimitive.Trigger.displayName - -const TabsContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -TabsContent.displayName = TabsPrimitive.Content.displayName - -export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/client-next/components/ui/tooltip.tsx b/client-next/components/ui/tooltip.tsx deleted file mode 100644 index 30fc44d..0000000 --- a/client-next/components/ui/tooltip.tsx +++ /dev/null @@ -1,30 +0,0 @@ -"use client" - -import * as React from "react" -import * as TooltipPrimitive from "@radix-ui/react-tooltip" - -import { cn } from "@/lib/utils" - -const TooltipProvider = TooltipPrimitive.Provider - -const Tooltip = TooltipPrimitive.Root - -const TooltipTrigger = TooltipPrimitive.Trigger - -const TooltipContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, sideOffset = 4, ...props }, ref) => ( - -)) -TooltipContent.displayName = TooltipPrimitive.Content.displayName - -export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/client-next/next.config.js b/client-next/next.config.js deleted file mode 100644 index 767719f..0000000 --- a/client-next/next.config.js +++ /dev/null @@ -1,4 +0,0 @@ -/** @type {import('next').NextConfig} */ -const nextConfig = {} - -module.exports = nextConfig diff --git a/client-next/package.json b/client-next/package.json deleted file mode 100644 index 7cf79fb..0000000 --- a/client-next/package.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "name": "client-next", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev -p 8080", - "build": "next build", - "start": "next start", - "lint": "next lint" - }, - "dependencies": { - "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-alert-dialog": "^1.0.5", - "@radix-ui/react-checkbox": "^1.0.4", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-select": "^2.0.0", - "@radix-ui/react-separator": "^1.0.3", - "@radix-ui/react-slot": "^1.0.2", - "@radix-ui/react-tabs": "^1.0.4", - "@radix-ui/react-toast": "^1.1.5", - "@radix-ui/react-tooltip": "^1.0.7", - "@tailwindcss/typography": "^0.5.10", - "class-variance-authority": "^0.7.0", - "clsx": "^2.0.0", - "lucide-react": "^0.284.0", - "markdown-to-jsx": "^7.3.2", - "next": "13.5.4", - "next-themes": "^0.2.1", - "react": "^18", - "react-dom": "^18", - "react-markdown": "^9.0.0", - "tailwind-merge": "^1.14.0", - "tailwindcss-animate": "^1.0.7" - }, - "devDependencies": { - "@types/node": "^20", - "@types/react": "^18", - "@types/react-dom": "^18", - "autoprefixer": "^10", - "eslint": "^8", - "eslint-config-next": "13.5.4", - "postcss": "^8", - "tailwindcss": "^3", - "typescript": "^5" - } -} diff --git a/client-next/tsconfig.json b/client-next/tsconfig.json deleted file mode 100644 index c714696..0000000 --- a/client-next/tsconfig.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "compilerOptions": { - "target": "es5", - "lib": ["dom", "dom.iterable", "esnext"], - "allowJs": true, - "skipLibCheck": true, - "strict": true, - "noEmit": true, - "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "bundler", - "resolveJsonModule": true, - "isolatedModules": true, - "jsx": "preserve", - "incremental": true, - "plugins": [ - { - "name": "next" - } - ], - "paths": { - "@/*": ["./*"] - } - }, - "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], - "exclude": ["node_modules"] -} diff --git a/client/.env.example b/client/.env.example index 7af6201..f592984 100644 --- a/client/.env.example +++ b/client/.env.example @@ -1,3 +1,3 @@ -VITE_OPEN_AI_API_URL="" -VITE_UNSPLASH_PROJECT_NAME="" -VITE_PROJECT_URL="" \ No newline at end of file +NEXT_PUBLIC_API_URL="" +NEXT_PUBLIC_UNSPLASH_PROJECT_NAME="" +NEXT_PUBLIC_PROJECT_URL="" \ No newline at end of file diff --git a/client/.eslintrc.cjs b/client/.eslintrc.cjs deleted file mode 100644 index 6ca0b86..0000000 --- a/client/.eslintrc.cjs +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - root: true, - env: { - browser: true, - es2020: true, - }, - extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:import/recommended", "plugin:react-hooks/recommended", "plugin:prettier/recommended"], - parser: "@typescript-eslint/parser", - plugins: ["react-refresh", "@typescript-eslint", "import", "unused-imports"], - rules: { - "prettier/prettier": [ - "error", - { - endOfLine: "auto", - }, - ], - "import/no-unused-modules": "error", - "unused-imports/no-unused-imports": "error", - "@typescript-eslint/no-unused-vars": ["error", { varsIgnorePattern: "^_" }], - "@typescript-eslint/no-explicit-any": "off", - "no-console": ["warn", { allow: ["warn", "error", "info", "debug"] }], - "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], - }, - ignorePatterns: ["dist", ".eslintrc.cjs"], - settings: { - "import/resolver": { - node: { - extensions: [".js", ".jsx", ".ts", ".tsx"], - }, - }, - }, -}; diff --git a/client/.eslintrc.json b/client/.eslintrc.json new file mode 100644 index 0000000..ebd1a1b --- /dev/null +++ b/client/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "root": true, + "extends": ["next/core-web-vitals", "plugin:@typescript-eslint/recommended", "prettier"], + "plugins": ["@typescript-eslint", "tailwindcss", "import", "unused-imports"], + "rules": { + "import/no-unused-modules": "error", + "unused-imports/no-unused-imports": "error", + "@typescript-eslint/no-unused-vars": ["error", { "varsIgnorePattern": "^_" }], + "@typescript-eslint/no-explicit-any": "off", + "no-console": ["warn", { "allow": ["warn", "error", "info", "debug"] }], + "@next/next/no-html-link-for-pages": "off" + }, + "settings": { + "tailwindcss": { + "callees": ["cn", "cva"], + "config": "tailwind.config.cjs" + }, + "next": { + "rootDir": ["apps/*/"] + } + }, + "overrides": [ + { + "files": ["*.ts", "*.tsx"], + "parser": "@typescript-eslint/parser" + }, + { + "files": ["components/ui/**/*.{tsx, ts}"], + "rules": { + "import/no-unused-modules": "off", + "unused-imports/no-unused-imports": "off", + "@typescript-eslint/no-unused-vars": "off" + } + } + ] +} diff --git a/client/.gitignore b/client/.gitignore index 51d8161..8f322f0 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -1,30 +1,35 @@ -# Logs -logs -*.log +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug npm-debug.log* yarn-debug.log* yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Environment variables -.env -.env.* -!.env.example - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? -package-lock.json \ No newline at end of file + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/client/.lintstagedrc.json b/client/.lintstagedrc.json index 624c6fe..f268fa1 100644 --- a/client/.lintstagedrc.json +++ b/client/.lintstagedrc.json @@ -1,3 +1,3 @@ { - "src/**/*.{ts,tsx,js,jsx,json,css,scss,md}": ["eslint --quiet --fix", "prettier --write"] + "./**/*.{ts,tsx,js,jsx,json,css,scss,md}": ["eslint --quiet --fix", "prettier --write"] } diff --git a/client/README.md b/client/README.md index d8fa05f..c403366 100644 --- a/client/README.md +++ b/client/README.md @@ -1,48 +1,36 @@ -# Make Me Project - Client +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). -The frontend of the DIY Project platform, built with modern technologies to provide users with engaging DIY project ideas. +## Getting Started -## 🚀 Getting Started - -### Prerequisites - -- [Node.js](https://nodejs.org/) -- [npm](https://www.npmjs.com/) - -### Installation - -1. Navigate to the client directory: +First, run the development server: ```bash -cd client +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev ``` -2. Install the dependencies: +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. -```bash -npm install -``` +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. -3. Set up your environment variables: +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. -> Copy the `.env.example` file and create a new `.env.development` or `.env.production` file with your own settings. +## Learn More -### Running the Server +To learn more about Next.js, take a look at the following resources: -For development: +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. -```bash -npm run dev -``` +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! -For production: +## Deploy on Vercel -```bash -npm run build -``` +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. -Then, preview the production build: - -```bash -npm run preview -``` +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/client-next/app/faq/faq.tsx b/client/app/faq/faq.tsx similarity index 99% rename from client-next/app/faq/faq.tsx rename to client/app/faq/faq.tsx index ac7e12c..ee3b246 100644 --- a/client-next/app/faq/faq.tsx +++ b/client/app/faq/faq.tsx @@ -19,7 +19,7 @@ export default function FAQPage() { {links?.[segment]?.text ?? segment} - ) + ), ); }; diff --git a/client-next/app/faq/page.tsx b/client/app/faq/page.tsx similarity index 100% rename from client-next/app/faq/page.tsx rename to client/app/faq/page.tsx diff --git a/client-next/app/globals.css b/client/app/globals.css similarity index 100% rename from client-next/app/globals.css rename to client/app/globals.css diff --git a/client-next/app/guide/[guide_name]/guide-name.tsx b/client/app/guide/[guide_name]/guide-name.tsx similarity index 98% rename from client-next/app/guide/[guide_name]/guide-name.tsx rename to client/app/guide/[guide_name]/guide-name.tsx index ab257e3..dc3b7f2 100644 --- a/client-next/app/guide/[guide_name]/guide-name.tsx +++ b/client/app/guide/[guide_name]/guide-name.tsx @@ -53,7 +53,7 @@ export default function HowToGuideDetail({ params }: { params: { guide_name: str {guideDetails && ( <> {guideDetails.metadata.title} - + )} diff --git a/client-next/app/guide/[guide_name]/page.tsx b/client/app/guide/[guide_name]/page.tsx similarity index 85% rename from client-next/app/guide/[guide_name]/page.tsx rename to client/app/guide/[guide_name]/page.tsx index ff957a9..37535ba 100644 --- a/client-next/app/guide/[guide_name]/page.tsx +++ b/client/app/guide/[guide_name]/page.tsx @@ -1,8 +1,8 @@ -import { Metadata, ResolvingMetadata } from "next"; +import { Metadata } from "next"; import { getGuideByPath } from "@/lib"; import HowToGuideDetail from "./guide-name"; -export async function generateMetadata({ params }: { params: { guide_name: string } }, parent: ResolvingMetadata): Promise { +export async function generateMetadata({ params }: { params: { guide_name: string } }): Promise { const guide = await getGuideByPath(params.guide_name); return { diff --git a/client-next/app/guide/guide.tsx b/client/app/guide/guide.tsx similarity index 100% rename from client-next/app/guide/guide.tsx rename to client/app/guide/guide.tsx diff --git a/client-next/app/guide/page.tsx b/client/app/guide/page.tsx similarity index 100% rename from client-next/app/guide/page.tsx rename to client/app/guide/page.tsx diff --git a/client-next/app/icon.ico b/client/app/icon.ico similarity index 100% rename from client-next/app/icon.ico rename to client/app/icon.ico diff --git a/client-next/app/layout.tsx b/client/app/layout.tsx similarity index 100% rename from client-next/app/layout.tsx rename to client/app/layout.tsx diff --git a/client-next/app/page.tsx b/client/app/page.tsx similarity index 73% rename from client-next/app/page.tsx rename to client/app/page.tsx index 2baafbd..f3d339e 100644 --- a/client-next/app/page.tsx +++ b/client/app/page.tsx @@ -1,27 +1,33 @@ "use client"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; -import BudgetFilter from "@/components/generate/budget-filter"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import dynamic from "next/dynamic"; + import CategoryFilter from "@/components/generate/category-filter"; import DifficultyFilter from "@/components/generate/difficulty-filter"; -import GenerateLoading from "@/components/generate/generate-loading"; import MaterialInput from "@/components/generate/material-input"; -import ProjectTabs from "@/components/generate/project-tabs"; import SafetyCheck from "@/components/generate/safety-check"; -import TimeAvailabilityFilter from "@/components/generate/time-availability-filter"; -import ToolsAvailableInput from "@/components/generate/tools-available-input"; -import { categories } from "@/constants"; -import { Button } from "@/components/ui/button"; -import PurposeFilter from "@/components/generate/purpose-filter"; -import { RefreshCcw } from "lucide-react"; -import { generateProjectIdeas, getTotalCountOfGeneratedIdea, incrementCounterOfGeneratedIdea } from "@/lib"; import { useToast } from "@/components/ui/use-toast"; import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { Button } from "@/components/ui/button"; +import { Info, RefreshCcw } from "lucide-react"; + +import { categories } from "@/constants"; +import { generateProjectIdeas, getTotalCountOfGeneratedIdea, incrementCounterOfGeneratedIdea } from "@/lib"; + +const TimeAvailabilityFilter = dynamic(() => import("@/components/generate/time-availability-filter")); +const ToolsAvailableInput = dynamic(() => import("@/components/generate/tools-available-input")); +const PurposeFilter = dynamic(() => import("@/components/generate/purpose-filter")); +const BudgetFilter = dynamic(() => import("@/components/generate/budget-filter")); + +const GenerateLoading = dynamic(() => import("@/components/generate/generate-loading")); +const ProjectTabs = dynamic(() => import("@/components/generate/project-tabs")); export default function Home() { const [materials, setMaterials] = useState([""]); const [onlySpecified, setOnlySpecified] = useState(false); - const [selectedDifficulty, setSelectedDifficulty] = useState("All"); + const [selectedDifficulty, setSelectedDifficulty] = useState("all"); const [selectedCategory, setSelectedCategory] = useState("Anything"); const [timeValue, setTimeValue] = useState(0); const [timeUnit, setTimeUnit] = useState(null); @@ -112,7 +118,7 @@ export default function Home() {
), - [purpose, timeUnit, timeValue, tools] + [purpose, timeUnit, timeValue, tools], ); const renderContent = () => { @@ -124,10 +130,12 @@ export default function Home() { return (
- +
+ +
); } @@ -151,7 +159,7 @@ export default function Home() {
- @@ -162,15 +170,19 @@ export default function Home() { return (
-

DIY Project Ideas

-
-
diff --git a/client/app/project-detail/[id]/page.tsx b/client/app/project-detail/[id]/page.tsx new file mode 100644 index 0000000..ac9728c --- /dev/null +++ b/client/app/project-detail/[id]/page.tsx @@ -0,0 +1,19 @@ +import { getShareLinkData } from "@/lib"; +import ProjectDetailById from "./project-detail-by-id"; +import { Metadata } from "next"; + +export async function generateMetadata({ params }: { params: { id: string } }): Promise { + const sharedLinkData = await getShareLinkData(params.id); + + return { + title: sharedLinkData.data.projectDetails.title + ` | MakeMeDIYspire`, + description: sharedLinkData.data.projectDetails.description, + keywords: ["DIY Project Details", "MakeMeDIYspire Tutorials", "DIY Project Instructions", "Step-by-Step DIY", "DIY Project Help", "DIY Creation Guide", "DIY Project Steps"], + metadataBase: new URL("https://www.diyspire/project-detail/" + params.id), + applicationName: "MakeMeDIYspire", + }; +} + +export default function Page({ params }: { params: { id: string } }) { + return ; +} diff --git a/client/app/project-detail/[id]/project-detail-by-id.tsx b/client/app/project-detail/[id]/project-detail-by-id.tsx new file mode 100644 index 0000000..9f47e2f --- /dev/null +++ b/client/app/project-detail/[id]/project-detail-by-id.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { useRouter } from "next/navigation"; +import ProjectImage from "@/components/project-detail/project-image"; +import ShareDialog from "@/components/project-detail/share-dialog"; +import ProjectInfo from "@/components/project-detail/project-info"; +import ProjectSteps from "@/components/project-detail/project-step"; +import { RelatedImages, ProjectLocationState } from "@/interfaces"; +import { useToast } from "@/components/ui/use-toast"; +import { getShareLinkData } from "@/lib"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; + +export default function ProjectDetailById({ params }: { params: { id: string } }) { + const router = useRouter(); + + const [isLoading, setIsLoading] = useState(true); + const [projectExplanation, setProjectExplanation] = useState(null); + const [relatedImages, setRelatedImages] = useState(null); + const [shareLink, setShareLink] = useState(null); + const [project, setProject] = useState(null); + const [isOpen, setIsOpen] = useState(false); + + const { toast } = useToast(); + + useEffect(() => { + async function fetchData() { + try { + if (params.id) { + const response = await getShareLinkData(params.id); + setProjectExplanation(response.data.explanation); + setRelatedImages(response.data.projectImage); + setProject(response.data.projectDetails); + } else { + return; + } + } catch (error: any) { + toast({ + title: "Data Fetch Error", + description: error.message, + }); + } finally { + setIsLoading(false); + } + } + + fetchData(); + }, [params.id, project, toast]); + + if (!project) { + return ( +
+
+ + +
+
+ ); + } + + return ( +
+
+ { + setShareLink(`${process.env.NEXT_PUBLIC_PROJECT_URL}/project-detail/${params.id}`); + setIsOpen(true); + }} + /> + setIsOpen(false)} isSaving={false} shareLink={shareLink} /> + +
+ + +
+ ); +} diff --git a/client/app/project-detail/page.tsx b/client/app/project-detail/page.tsx new file mode 100644 index 0000000..7e86cce --- /dev/null +++ b/client/app/project-detail/page.tsx @@ -0,0 +1,14 @@ +import { Metadata } from "next"; +import ProjectDetail from "./project-detail"; + +export const metadata: Metadata = { + title: "AI-Generated DIY Project Details - MakeMeDIYspire", + description: "Discover AI-generated DIY project details and get inspired with MakeMeDIYspire's innovative project generator.", + keywords: ["AI-Generated Projects", "DIY Project Details", "MakeMeDIYspire AI", "AI-Generated DIY Ideas", "AI-Generated DIY Projects"], + metadataBase: new URL("https://www.diyspire/project-detail"), + applicationName: "MakeMeDIYspire", +}; + +export default function Page() { + return ; +} diff --git a/client/app/project-detail/project-detail.tsx b/client/app/project-detail/project-detail.tsx new file mode 100644 index 0000000..ed16ea4 --- /dev/null +++ b/client/app/project-detail/project-detail.tsx @@ -0,0 +1,121 @@ +"use client"; + +import { useState, useEffect, useCallback } from "react"; +import { useSearchParams, useRouter } from "next/navigation"; +import { useToast } from "@/components/ui/use-toast"; +import { RelatedImages, ProjectLocationState } from "@/interfaces"; +import ProjectImage from "@/components/project-detail/project-image"; +import ShareDialog from "@/components/project-detail/share-dialog"; +import ProjectInfo from "@/components/project-detail/project-info"; +import ProjectSteps from "@/components/project-detail/project-step"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Separator } from "@/components/ui/separator"; +import { generateProjectExplanations, saveShareLinkData, searchImages } from "@/lib"; + +export default function ProjectDetail() { + const searchParams = useSearchParams(); + const projectParams: ProjectLocationState = JSON.parse(searchParams.get("project") as string); + const router = useRouter(); + const [isLoading, setIsLoading] = useState(true); + const [projectExplanation, setProjectExplanation] = useState(null); + const [relatedImages, setRelatedImages] = useState(null); + const [isSaving, setIsSaving] = useState(false); + const [shareLink, setShareLink] = useState(null); + const [project] = useState(projectParams); + const [isOpen, setIsOpen] = useState(false); + + const { toast } = useToast(); + + useEffect(() => { + async function fetchData() { + try { + if (project) { + const [explanationResult, imageResult] = await Promise.allSettled([ + generateProjectExplanations(project.title, project.materials, project.tools, project.time, project.budget, project.description), + searchImages(project.title), + ]); + + if (explanationResult.status === "fulfilled") { + setProjectExplanation(explanationResult.value.data.explanation); + } else { + toast({ title: "Explanation Error", description: "Failed to generate project explanation." }); + } + + if (imageResult.status === "fulfilled") { + setRelatedImages(imageResult.value.data); + } else { + toast({ title: "Image Search Error", description: "Failed to fetch related images." }); + } + } else { + return; + } + } catch (error) { + toast({ title: "Data Fetch Error", description: "An error occurred while fetching project data." }); + } finally { + setIsLoading(false); + } + } + + fetchData(); + }, [project, toast]); + + const handleSaveProject = useCallback(async () => { + try { + if (shareLink) { + return; + } + + setIsSaving(true); + + const response = await saveShareLinkData(project, relatedImages, projectExplanation); + + setShareLink(`${process.env.NEXT_PUBLIC_PROJECT_URL}/project-detail/${response.data.id}`); + + toast({ + title: "Project Saved", + description: "The project details have been successfully saved.", + }); + } catch (error) { + toast({ + title: "Saving Error", + description: "An error occurred while saving the project details.", + }); + } finally { + setIsSaving(false); + } + }, [project, projectExplanation, relatedImages, shareLink, toast]); + + if (!project) { + return ( +
+
+ + +
+
+ ); + } + + return ( +
+
+ { + handleSaveProject(); + setIsOpen(true); + }} + /> + setIsOpen(false)} isSaving={isSaving} shareLink={shareLink} /> + +
+ + +
+ ); +} diff --git a/client-next/app/robots.ts b/client/app/robots.ts similarity index 100% rename from client-next/app/robots.ts rename to client/app/robots.ts diff --git a/client-next/components.json b/client/components.json similarity index 99% rename from client-next/components.json rename to client/components.json index 48c34e4..fd5076f 100644 --- a/client-next/components.json +++ b/client/components.json @@ -13,4 +13,4 @@ "components": "@/components", "utils": "@/lib/utils" } -} \ No newline at end of file +} diff --git a/client-next/components/custom-markdown.tsx b/client/components/custom-markdown.tsx similarity index 100% rename from client-next/components/custom-markdown.tsx rename to client/components/custom-markdown.tsx diff --git a/client/components/footer.tsx b/client/components/footer.tsx new file mode 100644 index 0000000..ec49300 --- /dev/null +++ b/client/components/footer.tsx @@ -0,0 +1,49 @@ +import Link from "next/link"; +import { footerData } from "@/constants"; + +const Footer: React.FC = () => { + return ( +
+
+
+ {footerData.map((data, index) => ( +
+ +
+ {data.links.map((link, linkIndex) => ( + + ))} +
+
+ ))} +
+ +
+

+ Powered by MakeMeDIYspire ✨ | Made with ❤️ by -{" "} + + Kooma + +

+
+
+
+ ); +}; + +export default Footer; diff --git a/client-next/components/generate/budget-filter.tsx b/client/components/generate/budget-filter.tsx similarity index 60% rename from client-next/components/generate/budget-filter.tsx rename to client/components/generate/budget-filter.tsx index 1a28a3b..30235ca 100644 --- a/client-next/components/generate/budget-filter.tsx +++ b/client/components/generate/budget-filter.tsx @@ -12,15 +12,19 @@ const BudgetFilter: React.FC = ({ onBudgetChange, className } (event: React.ChangeEvent) => { onBudgetChange(Number(event.target.value)); }, - [onBudgetChange] + [onBudgetChange], ); return (
- - +
+ + + + +
); }; diff --git a/client-next/components/generate/category-filter.tsx b/client/components/generate/category-filter.tsx similarity index 89% rename from client-next/components/generate/category-filter.tsx rename to client/components/generate/category-filter.tsx index af480fe..2c9da26 100644 --- a/client-next/components/generate/category-filter.tsx +++ b/client/components/generate/category-filter.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { categoryIcons } from "@/constants"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; @@ -20,7 +20,7 @@ const CategoryFilter: React.FC = ({ categories, onCategoryC return (
- +
{categories.map((category) => { @@ -32,6 +32,7 @@ const CategoryFilter: React.FC = ({ categories, onCategoryC variant="outline" onClick={() => handleCategoryClick(category)} className={`p-2 border cursor-pointer ${isSelected ? "text-black bg-secondary dark:text-white" : ""} flex items-center justify-center space-x-2 transition duration-300 ease-in-out`} + aria-label={`Filter by ${category} category`} > {IconComponent && } {category} diff --git a/client-next/components/generate/difficulty-filter.tsx b/client/components/generate/difficulty-filter.tsx similarity index 75% rename from client-next/components/generate/difficulty-filter.tsx rename to client/components/generate/difficulty-filter.tsx index c6ae463..b71c996 100644 --- a/client-next/components/generate/difficulty-filter.tsx +++ b/client/components/generate/difficulty-filter.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import { useState } from "react"; import { difficulties } from "@/constants"; import { Button } from "@/components/ui/button"; import { Label } from "@/components/ui/label"; @@ -19,7 +19,7 @@ const DifficultyFilter: React.FC = ({ onDifficultyChange, return (
- +
{difficulties.map(({ level, icon: IconComponent }) => ( @@ -27,7 +27,10 @@ const DifficultyFilter: React.FC = ({ onDifficultyChange, key={level} variant="outline" onClick={() => handleDifficultyClick(level)} - className={`p-2 border cursor-pointer ${selectedDifficulty === level ? "font-bold" : "font-normal"} flex items-center justify-center space-x-2 transition duration-300 ease-in-out`} + className={`p-2 border cursor-pointer ${ + selectedDifficulty === level ? "text-black bg-secondary dark:text-white" : "" + } flex items-center justify-center space-x-2 transition duration-300 ease-in-out`} + aria-label={`Filter by ${level} difficulty`} > {IconComponent && } {level} diff --git a/client/components/generate/generate-loading.tsx b/client/components/generate/generate-loading.tsx new file mode 100644 index 0000000..d7ddecd --- /dev/null +++ b/client/components/generate/generate-loading.tsx @@ -0,0 +1,34 @@ +import { useState } from "react"; +import { loadingMessages } from "@/constants"; +import useInterval from "@/hooks/useInterval"; +import { Info, Loader } from "lucide-react"; +import { Label } from "@/components/ui/label"; + +const GenerateLoading: React.FC = () => { + const [currentMessageIndex, setCurrentMessageIndex] = useState(0); + + useInterval(() => { + setCurrentMessageIndex((prevIndex) => (prevIndex + 1) % loadingMessages.length); + }, 5000); + + return ( +
+ +

+ {loadingMessages[currentMessageIndex]} +

+
+ + + + +
+
+ ); +}; + +export default GenerateLoading; diff --git a/client-next/components/generate/material-input.tsx b/client/components/generate/material-input.tsx similarity index 95% rename from client-next/components/generate/material-input.tsx rename to client/components/generate/material-input.tsx index e6e56f0..693c0ea 100644 --- a/client-next/components/generate/material-input.tsx +++ b/client/components/generate/material-input.tsx @@ -1,5 +1,5 @@ import { useCallback } from "react"; -import { Plus, PlusCircle, X } from "lucide-react"; +import { PlusCircle, X } from "lucide-react"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; @@ -20,7 +20,7 @@ const MaterialInput: React.FC = ({ materials, setMaterials, updatedMaterials[index] = value; setMaterials(updatedMaterials); }, - [materials, setMaterials] + [materials, setMaterials], ); const handleDeleteInput = useCallback( @@ -29,7 +29,7 @@ const MaterialInput: React.FC = ({ materials, setMaterials, updatedMaterials.splice(index, 1); setMaterials(updatedMaterials); }, - [materials, setMaterials] + [materials, setMaterials], ); const handleAddMore = useCallback(() => { diff --git a/client-next/components/generate/project-tabs.tsx b/client/components/generate/project-tabs.tsx similarity index 84% rename from client-next/components/generate/project-tabs.tsx rename to client/components/generate/project-tabs.tsx index ffb0604..94ee8b9 100644 --- a/client-next/components/generate/project-tabs.tsx +++ b/client/components/generate/project-tabs.tsx @@ -1,9 +1,9 @@ import Link from "next/link"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Separator } from "@/components/ui/separator"; import { Badge } from "@/components/ui/badge"; import { Label } from "@/components/ui/label"; import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; import { BookOpen } from "lucide-react"; interface Project { @@ -26,16 +26,16 @@ const ProjectTabs: React.FC = ({ projects, className }) => { {projects.map((project) => ( - + {project.title} ))} {projects.map((project) => ( -
-

{project.title}

- +
+

{project.title}

+
{project.tags.map((tag, tagIndex) => ( @@ -44,7 +44,7 @@ const ProjectTabs: React.FC = ({ projects, className }) => { ))}
- + {/* Hide the separator on small screens */}

{project.description}

- -
+ {/* Display the separator on medium and larger screens */} +
diff --git a/client/components/project-detail/project-image.tsx b/client/components/project-detail/project-image.tsx new file mode 100644 index 0000000..102e6e7 --- /dev/null +++ b/client/components/project-detail/project-image.tsx @@ -0,0 +1,92 @@ +import { RelatedImages } from "@/interfaces"; +import { Button } from "@/components/ui/button"; +import { Label } from "@/components/ui/label"; +import { Skeleton } from "@/components/ui/skeleton"; +import Image from "next/image"; +import { Info, Share } from "lucide-react"; + +interface ProjectImageProps { + isLoading: boolean; + projectTitle: string; + relatedImages: RelatedImages | null; + onOpen: () => void; +} + +const UNSPLASH_PROJECT_NAME = process.env.NEXT_PUBLIC_UNSPLASH_PROJECT_NAME; + +const ProjectImage: React.FC = ({ isLoading, projectTitle, relatedImages, onOpen }) => { + const imageUrl = relatedImages?.urls ? relatedImages.urls.regular : "https://via.placeholder.com/500"; + const photographerName = relatedImages?.user?.name; + const photographerLink = relatedImages?.user?.link; + + const googleSearchLink = `https://www.google.com/search?q=${projectTitle}`; + const youtubeSearchLink = `https://www.youtube.com/results?search_query=${projectTitle} Tutorial`; + + if (isLoading) { + return ( +
+ +
+ + + + +
+
+ ); + } + + return ( +
+ {relatedImages?.alt_description e.preventDefault()} + loading="eager" + /> + +
+ {photographerName && ( + + )} +
+ +
+ +

+ Note: The image may not accurately represent the project title. For more specific results, you can click to{" "} + + search on Google + {" "} + or watch tutorials on{" "} + + YouTube + + . +

+
+ + +
+ ); +}; + +export default ProjectImage; diff --git a/client/components/project-detail/project-info.tsx b/client/components/project-detail/project-info.tsx new file mode 100644 index 0000000..5aef546 --- /dev/null +++ b/client/components/project-detail/project-info.tsx @@ -0,0 +1,62 @@ +import { ProjectLocationState } from "@/interfaces"; +import { Skeleton } from "@/components/ui/skeleton"; +import { Label } from "@/components/ui/label"; +import { TimerIcon } from "lucide-react"; + +interface ProjectInfoProps { + isLoading: boolean; + project: ProjectLocationState; +} + +const ProjectInfo: React.FC = ({ isLoading, project }) => { + if (isLoading) { + return ( +
+
+ ); + } + + return ( +
+
+

{project.title}

+ +
+ + +
+
+ +
+
+ +
+ +
+ +
    + {project.materials.map((material, index) => ( +
  • {material}
  • + ))} +
+
+ +
+ +
    {project.tools.length >= 1 ? project.tools.map((tool, index) =>
  • {tool}
  • ) :
  • Nothing
  • }
+
+
+
+ ); +}; + +export default ProjectInfo; diff --git a/client/components/project-detail/project-step.tsx b/client/components/project-detail/project-step.tsx new file mode 100644 index 0000000..d3f3655 --- /dev/null +++ b/client/components/project-detail/project-step.tsx @@ -0,0 +1,43 @@ +import React from "react"; +import { Label } from "@/components/ui/label"; +import { Skeleton } from "@/components/ui/skeleton"; + +interface ProjectStepsProps { + isLoading: boolean; + projectExplanation: string | null; +} + +const ProjectSteps: React.FC = ({ isLoading, projectExplanation }) => { + return ( +
+ {isLoading ? ( + <> + +
+ {[...Array(5)].map((_, i) => ( +
+ +
+ ))} +
+ + ) : ( + <> + + + + )} +
+ ); +}; + +export default ProjectSteps; diff --git a/client/components/project-detail/share-dialog.tsx b/client/components/project-detail/share-dialog.tsx new file mode 100644 index 0000000..36c7daf --- /dev/null +++ b/client/components/project-detail/share-dialog.tsx @@ -0,0 +1,92 @@ +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogDescription } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { useToast } from "@/components/ui/use-toast"; +import useClipboard from "@/hooks/useClipboard"; +import { Check, Copy, Share2 } from "lucide-react"; + +interface ShareDialogProps { + isOpen: boolean; + onClose: () => void; + isSaving: boolean; + shareLink: string | null; +} + +const ShareDialog: React.FC = ({ isOpen, onClose, isSaving, shareLink }) => { + const { hasCopied, onCopy } = useClipboard(shareLink ?? ""); + const { toast } = useToast(); + + const handleWebShare = async () => { + if (navigator.share && shareLink) { + try { + await navigator.share({ + title: "Share this project", + url: shareLink, + }); + } catch (error) { + toast({ + title: "Share Failed", + description: "There was an error while sharing.", + }); + } + } else { + toast({ + title: "Share Unavailable", + description: "Web Share API is not supported in this browser.", + }); + } + }; + + const renderModalContent = () => { + if (isSaving) { + return ( +
+
+ +
+ ); + } + + if (shareLink) { + return ( +
+ +
+ + +
+
+ ); + } + + return null; + }; + + return ( + + + + {!isSaving && ( + <> + + Share this project + + Use the link below to share this project. + + )} + +
{renderModalContent()}
+ + + +
+
+ ); +}; + +export default ShareDialog; diff --git a/client-next/components/theme-provider.tsx b/client/components/theme-provider.tsx similarity index 100% rename from client-next/components/theme-provider.tsx rename to client/components/theme-provider.tsx diff --git a/client/components/ui/accordion.tsx b/client/components/ui/accordion.tsx new file mode 100644 index 0000000..810db3d --- /dev/null +++ b/client/components/ui/accordion.tsx @@ -0,0 +1,45 @@ +"use client"; + +import * as React from "react"; +import * as AccordionPrimitive from "@radix-ui/react-accordion"; +import { ChevronDown } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Accordion = AccordionPrimitive.Root; + +const AccordionItem = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +AccordionItem.displayName = "AccordionItem"; + +const AccordionTrigger = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, children, ...props }, ref) => ( + + svg]:rotate-180", className)} + {...props} + > + {children} + + + + ), +); +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName; + +const AccordionContent = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, children, ...props }, ref) => ( + +
{children}
+
+ ), +); +AccordionContent.displayName = AccordionPrimitive.Content.displayName; + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent }; diff --git a/client/components/ui/alert-dialog.tsx b/client/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..ca484ca --- /dev/null +++ b/client/components/ui/alert-dialog.tsx @@ -0,0 +1,86 @@ +"use client"; + +import * as React from "react"; +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"; + +import { cn } from "@/lib/utils"; +import { buttonVariants } from "@/components/ui/button"; + +const AlertDialog = AlertDialogPrimitive.Root; + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger; + +const AlertDialogPortal = AlertDialogPrimitive.Portal; + +const AlertDialogOverlay = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, children, ...props }, ref) => ( + + ), +); +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName; + +const AlertDialogContent = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, ...props }, ref) => ( + + + + + ), +); +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName; + +const AlertDialogHeader = ({ className, ...props }: React.HTMLAttributes) =>
; +AlertDialogHeader.displayName = "AlertDialogHeader"; + +const AlertDialogFooter = ({ className, ...props }: React.HTMLAttributes) => ( +
+); +AlertDialogFooter.displayName = "AlertDialogFooter"; + +const AlertDialogTitle = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName; + +const AlertDialogDescription = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, ...props }, ref) => , +); +AlertDialogDescription.displayName = AlertDialogPrimitive.Description.displayName; + +const AlertDialogAction = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName; + +const AlertDialogCancel = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName; + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +}; diff --git a/client/components/ui/alert.tsx b/client/components/ui/alert.tsx new file mode 100644 index 0000000..630d000 --- /dev/null +++ b/client/components/ui/alert.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const alertVariants = cva("relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, +}); + +const Alert = React.forwardRef & VariantProps>(({ className, variant, ...props }, ref) => ( +
+)); +Alert.displayName = "Alert"; + +const AlertTitle = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +AlertTitle.displayName = "AlertTitle"; + +const AlertDescription = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +AlertDescription.displayName = "AlertDescription"; + +export { Alert, AlertTitle, AlertDescription }; diff --git a/client/components/ui/badge.tsx b/client/components/ui/badge.tsx new file mode 100644 index 0000000..fd0f6f1 --- /dev/null +++ b/client/components/ui/badge.tsx @@ -0,0 +1,26 @@ +import * as React from "react"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const badgeVariants = cva("inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", { + variants: { + variant: { + default: "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, +}); + +export interface BadgeProps extends React.HTMLAttributes, VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return
; +} + +export { Badge, badgeVariants }; diff --git a/client-next/components/ui/button.tsx b/client/components/ui/button.tsx similarity index 50% rename from client-next/components/ui/button.tsx rename to client/components/ui/button.tsx index ac8e0c9..5b14133 100644 --- a/client-next/components/ui/button.tsx +++ b/client/components/ui/button.tsx @@ -1,8 +1,8 @@ -import * as React from "react" -import { Slot } from "@radix-ui/react-slot" -import { cva, type VariantProps } from "class-variance-authority" +import * as React from "react"; +import { Slot } from "@radix-ui/react-slot"; +import { cva, type VariantProps } from "class-variance-authority"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; const buttonVariants = cva( "inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", @@ -10,12 +10,9 @@ const buttonVariants = cva( variants: { variant: { default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: "bg-secondary text-secondary-foreground hover:bg-secondary/80", ghost: "hover:bg-accent hover:text-accent-foreground", link: "text-primary underline-offset-4 hover:underline", }, @@ -30,27 +27,17 @@ const buttonVariants = cva( variant: "default", size: "default", }, - } -) + }, +); -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean +export interface ButtonProps extends React.ButtonHTMLAttributes, VariantProps { + asChild?: boolean; } -const Button = React.forwardRef( - ({ className, variant, size, asChild = false, ...props }, ref) => { - const Comp = asChild ? Slot : "button" - return ( - - ) - } -) -Button.displayName = "Button" +const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : "button"; + return ; +}); +Button.displayName = "Button"; -export { Button, buttonVariants } +export { Button, buttonVariants }; diff --git a/client/components/ui/card.tsx b/client/components/ui/card.tsx new file mode 100644 index 0000000..5918514 --- /dev/null +++ b/client/components/ui/card.tsx @@ -0,0 +1,33 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +const Card = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +Card.displayName = "Card"; + +const CardHeader = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +CardHeader.displayName = "CardHeader"; + +const CardTitle = React.forwardRef>(({ className, ...props }, ref) => ( +

+)); +CardTitle.displayName = "CardTitle"; + +const CardDescription = React.forwardRef>(({ className, ...props }, ref) => ( +

+)); +CardDescription.displayName = "CardDescription"; + +const CardContent = React.forwardRef>(({ className, ...props }, ref) =>

); +CardContent.displayName = "CardContent"; + +const CardFooter = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); +CardFooter.displayName = "CardFooter"; + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }; diff --git a/client/components/ui/checkbox.tsx b/client/components/ui/checkbox.tsx new file mode 100644 index 0000000..368a72e --- /dev/null +++ b/client/components/ui/checkbox.tsx @@ -0,0 +1,25 @@ +"use client"; + +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Checkbox = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/client/components/ui/dialog.tsx b/client/components/ui/dialog.tsx new file mode 100644 index 0000000..58a5a55 --- /dev/null +++ b/client/components/ui/dialog.tsx @@ -0,0 +1,64 @@ +"use client"; + +import * as React from "react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { X } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Dialog = DialogPrimitive.Root; + +const DialogTrigger = DialogPrimitive.Trigger; + +const DialogPortal = DialogPrimitive.Portal; + +const DialogOverlay = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; + +const DialogContent = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, children, ...props }, ref) => ( + + + + {children} + + + Close + + + +)); +DialogContent.displayName = DialogPrimitive.Content.displayName; + +const DialogHeader = ({ className, ...props }: React.HTMLAttributes) =>
; +DialogHeader.displayName = "DialogHeader"; + +const DialogFooter = ({ className, ...props }: React.HTMLAttributes) =>
; +DialogFooter.displayName = "DialogFooter"; + +const DialogTitle = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +DialogTitle.displayName = DialogPrimitive.Title.displayName; + +const DialogDescription = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +DialogDescription.displayName = DialogPrimitive.Description.displayName; + +export { Dialog, DialogPortal, DialogOverlay, DialogTrigger, DialogContent, DialogHeader, DialogFooter, DialogTitle, DialogDescription }; diff --git a/client/components/ui/input.tsx b/client/components/ui/input.tsx new file mode 100644 index 0000000..6fdd7bd --- /dev/null +++ b/client/components/ui/input.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; + +import { cn } from "@/lib/utils"; + +export interface InputProps extends React.InputHTMLAttributes {} + +const Input = React.forwardRef(({ className, type, ...props }, ref) => { + return ( + + ); +}); +Input.displayName = "Input"; + +export { Input }; diff --git a/client/components/ui/label.tsx b/client/components/ui/label.tsx new file mode 100644 index 0000000..e80c9cb --- /dev/null +++ b/client/components/ui/label.tsx @@ -0,0 +1,16 @@ +"use client"; + +import * as React from "react"; +import * as LabelPrimitive from "@radix-ui/react-label"; +import { cva, type VariantProps } from "class-variance-authority"; + +import { cn } from "@/lib/utils"; + +const labelVariants = cva("text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"); + +const Label = React.forwardRef, React.ComponentPropsWithoutRef & VariantProps>( + ({ className, ...props }, ref) => , +); +Label.displayName = LabelPrimitive.Root.displayName; + +export { Label }; diff --git a/client/components/ui/select.tsx b/client/components/ui/select.tsx new file mode 100644 index 0000000..cb3587c --- /dev/null +++ b/client/components/ui/select.tsx @@ -0,0 +1,84 @@ +"use client"; + +import * as React from "react"; +import * as SelectPrimitive from "@radix-ui/react-select"; +import { Check, ChevronDown } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Select = SelectPrimitive.Root; + +const SelectGroup = SelectPrimitive.Group; + +const SelectValue = SelectPrimitive.Value; + +const SelectTrigger = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, children, ...props }, ref) => ( + + {children} + + + + +)); +SelectTrigger.displayName = SelectPrimitive.Trigger.displayName; + +const SelectContent = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, children, position = "popper", ...props }, ref) => ( + + + + {children} + + + + ), +); +SelectContent.displayName = SelectPrimitive.Content.displayName; + +const SelectLabel = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +SelectLabel.displayName = SelectPrimitive.Label.displayName; + +const SelectItem = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, children, ...props }, ref) => ( + + + + + + + + {children} + +)); +SelectItem.displayName = SelectPrimitive.Item.displayName; + +const SelectSeparator = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +SelectSeparator.displayName = SelectPrimitive.Separator.displayName; + +export { Select, SelectGroup, SelectValue, SelectTrigger, SelectContent, SelectLabel, SelectItem, SelectSeparator }; diff --git a/client/components/ui/separator.tsx b/client/components/ui/separator.tsx new file mode 100644 index 0000000..5459436 --- /dev/null +++ b/client/components/ui/separator.tsx @@ -0,0 +1,21 @@ +"use client"; + +import * as React from "react"; +import * as SeparatorPrimitive from "@radix-ui/react-separator"; + +import { cn } from "@/lib/utils"; + +const Separator = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, orientation = "horizontal", decorative = true, ...props }, ref) => ( + + ), +); +Separator.displayName = SeparatorPrimitive.Root.displayName; + +export { Separator }; diff --git a/client/components/ui/skeleton.tsx b/client/components/ui/skeleton.tsx new file mode 100644 index 0000000..6690a13 --- /dev/null +++ b/client/components/ui/skeleton.tsx @@ -0,0 +1,7 @@ +import { cn } from "@/lib/utils"; + +function Skeleton({ className, ...props }: React.HTMLAttributes) { + return
; +} + +export { Skeleton }; diff --git a/client/components/ui/tabs.tsx b/client/components/ui/tabs.tsx new file mode 100644 index 0000000..32cd31d --- /dev/null +++ b/client/components/ui/tabs.tsx @@ -0,0 +1,36 @@ +"use client"; + +import * as React from "react"; +import * as TabsPrimitive from "@radix-ui/react-tabs"; + +import { cn } from "@/lib/utils"; + +const Tabs = TabsPrimitive.Root; + +const TabsList = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef, React.ComponentPropsWithoutRef>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent }; diff --git a/client-next/components/ui/toast.tsx b/client/components/ui/toast.tsx similarity index 99% rename from client-next/components/ui/toast.tsx rename to client/components/ui/toast.tsx index 89a4980..2babf47 100644 --- a/client-next/components/ui/toast.tsx +++ b/client/components/ui/toast.tsx @@ -24,13 +24,13 @@ const toastVariants = cva( defaultVariants: { variant: "default", }, - } + }, ); const Toast = React.forwardRef, React.ComponentPropsWithoutRef & VariantProps>( ({ className, variant, ...props }, ref) => { return ; - } + }, ); Toast.displayName = ToastPrimitives.Root.displayName; @@ -39,7 +39,7 @@ const ToastAction = React.forwardRef @@ -51,7 +51,7 @@ const ToastClose = React.forwardRef @@ -20,16 +13,14 @@ export function Toaster() {
{title && {title}} - {description && ( - {description} - )} + {description && {description}}
{action}
- ) + ); })} - ) + ); } diff --git a/client/components/ui/tooltip.tsx b/client/components/ui/tooltip.tsx new file mode 100644 index 0000000..058a310 --- /dev/null +++ b/client/components/ui/tooltip.tsx @@ -0,0 +1,29 @@ +"use client"; + +import * as React from "react"; +import * as TooltipPrimitive from "@radix-ui/react-tooltip"; + +import { cn } from "@/lib/utils"; + +const TooltipProvider = TooltipPrimitive.Provider; + +const Tooltip = TooltipPrimitive.Root; + +const TooltipTrigger = TooltipPrimitive.Trigger; + +const TooltipContent = React.forwardRef, React.ComponentPropsWithoutRef>( + ({ className, sideOffset = 4, ...props }, ref) => ( + + ), +); +TooltipContent.displayName = TooltipPrimitive.Content.displayName; + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }; diff --git a/client-next/components/ui/use-toast.ts b/client/components/ui/use-toast.ts similarity index 57% rename from client-next/components/ui/use-toast.ts rename to client/components/ui/use-toast.ts index 90d8959..ed177ab 100644 --- a/client-next/components/ui/use-toast.ts +++ b/client/components/ui/use-toast.ts @@ -1,76 +1,73 @@ // Inspired by react-hot-toast library -import * as React from "react" +import * as React from "react"; -import type { - ToastActionElement, - ToastProps, -} from "@/components/ui/toast" +import type { ToastActionElement, ToastProps } from "@/components/ui/toast"; -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 +const TOAST_LIMIT = 1; +const TOAST_REMOVE_DELAY = 1000000; type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + action?: ToastActionElement; +}; const actionTypes = { ADD_TOAST: "ADD_TOAST", UPDATE_TOAST: "UPDATE_TOAST", DISMISS_TOAST: "DISMISS_TOAST", REMOVE_TOAST: "REMOVE_TOAST", -} as const +} as const; -let count = 0 +let count = 0; function genId() { - count = (count + 1) % Number.MAX_VALUE - return count.toString() + count = (count + 1) % Number.MAX_VALUE; + return count.toString(); } -type ActionType = typeof actionTypes +type ActionType = typeof actionTypes; type Action = | { - type: ActionType["ADD_TOAST"] - toast: ToasterToast + type: ActionType["ADD_TOAST"]; + toast: ToasterToast; } | { - type: ActionType["UPDATE_TOAST"] - toast: Partial + type: ActionType["UPDATE_TOAST"]; + toast: Partial; } | { - type: ActionType["DISMISS_TOAST"] - toastId?: ToasterToast["id"] + type: ActionType["DISMISS_TOAST"]; + toastId?: ToasterToast["id"]; } | { - type: ActionType["REMOVE_TOAST"] - toastId?: ToasterToast["id"] - } + type: ActionType["REMOVE_TOAST"]; + toastId?: ToasterToast["id"]; + }; interface State { - toasts: ToasterToast[] + toasts: ToasterToast[]; } -const toastTimeouts = new Map>() +const toastTimeouts = new Map>(); const addToRemoveQueue = (toastId: string) => { if (toastTimeouts.has(toastId)) { - return + return; } const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) + toastTimeouts.delete(toastId); dispatch({ type: "REMOVE_TOAST", toastId: toastId, - }) - }, TOAST_REMOVE_DELAY) + }); + }, TOAST_REMOVE_DELAY); - toastTimeouts.set(toastId, timeout) -} + toastTimeouts.set(toastId, timeout); +}; export const reducer = (state: State, action: Action): State => { switch (action.type) { @@ -78,27 +75,25 @@ export const reducer = (state: State, action: Action): State => { return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } + }; case "UPDATE_TOAST": return { ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - } + toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)), + }; case "DISMISS_TOAST": { - const { toastId } = action + const { toastId } = action; // ! Side effects ! - This could be extracted into a dismissToast() action, // but I'll keep it here for simplicity if (toastId) { - addToRemoveQueue(toastId) + addToRemoveQueue(toastId); } else { state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) + addToRemoveQueue(toast.id); + }); } return { @@ -109,46 +104,46 @@ export const reducer = (state: State, action: Action): State => { ...t, open: false, } - : t + : t, ), - } + }; } case "REMOVE_TOAST": if (action.toastId === undefined) { return { ...state, toasts: [], - } + }; } return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId), - } + }; } -} +}; -const listeners: Array<(state: State) => void> = [] +const listeners: Array<(state: State) => void> = []; -let memoryState: State = { toasts: [] } +let memoryState: State = { toasts: [] }; function dispatch(action: Action) { - memoryState = reducer(memoryState, action) + memoryState = reducer(memoryState, action); listeners.forEach((listener) => { - listener(memoryState) - }) + listener(memoryState); + }); } -type Toast = Omit +type Toast = Omit; function toast({ ...props }: Toast) { - const id = genId() + const id = genId(); const update = (props: ToasterToast) => dispatch({ type: "UPDATE_TOAST", toast: { ...props, id }, - }) - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + }); + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); dispatch({ type: "ADD_TOAST", @@ -157,36 +152,36 @@ function toast({ ...props }: Toast) { id, open: true, onOpenChange: (open) => { - if (!open) dismiss() + if (!open) dismiss(); }, }, - }) + }); return { id: id, dismiss, update, - } + }; } function useToast() { - const [state, setState] = React.useState(memoryState) + const [state, setState] = React.useState(memoryState); React.useEffect(() => { - listeners.push(setState) + listeners.push(setState); return () => { - const index = listeners.indexOf(setState) + const index = listeners.indexOf(setState); if (index > -1) { - listeners.splice(index, 1) + listeners.splice(index, 1); } - } - }, [state]) + }; + }, [state]); return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - } + }; } -export { useToast, toast } +export { useToast, toast }; diff --git a/client-next/constants/index.ts b/client/constants/index.ts similarity index 100% rename from client-next/constants/index.ts rename to client/constants/index.ts diff --git a/client/hooks/useClipboard.ts b/client/hooks/useClipboard.ts new file mode 100644 index 0000000..73f5187 --- /dev/null +++ b/client/hooks/useClipboard.ts @@ -0,0 +1,19 @@ +import { useState, useCallback } from "react"; + +const useClipboard = (text: string) => { + const [hasCopied, setHasCopied] = useState(false); + + const onCopy = useCallback(async () => { + try { + await navigator.clipboard.writeText(text); + setHasCopied(true); + } catch (err) { + console.error("Failed to copy text: ", err); + setHasCopied(false); + } + }, [text]); + + return { hasCopied, onCopy }; +}; + +export default useClipboard; diff --git a/client-next/hooks/useInterval.ts b/client/hooks/useInterval.ts similarity index 91% rename from client-next/hooks/useInterval.ts rename to client/hooks/useInterval.ts index 881fad2..73c3fe4 100644 --- a/client-next/hooks/useInterval.ts +++ b/client/hooks/useInterval.ts @@ -13,7 +13,7 @@ function useInterval(callback: () => void, delay: number | null) { } if (delay !== null) { - let id = setInterval(tick, delay); + const id = setInterval(tick, delay); return () => clearInterval(id); } }, [delay]); diff --git a/client/index.html b/client/index.html deleted file mode 100644 index 52cf760..0000000 --- a/client/index.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - - - - - - - MakeMeDIYspire - AI-Powered DIY Project Idea Generator - - - - - - - - - - - - - - - - -
- - - diff --git a/client-next/interfaces/index.ts b/client/interfaces/index.ts similarity index 100% rename from client-next/interfaces/index.ts rename to client/interfaces/index.ts diff --git a/client-next/lib/index.ts b/client/lib/index.ts similarity index 90% rename from client-next/lib/index.ts rename to client/lib/index.ts index 845d828..250e910 100644 --- a/client-next/lib/index.ts +++ b/client/lib/index.ts @@ -1,3 +1,5 @@ +import { ProjectLocationState, RelatedImages } from "@/interfaces"; + const API_URL = process.env.NEXT_PUBLIC_API_URL; type FetchApiOptions = { @@ -38,7 +40,7 @@ export const generateProjectIdeas = async ( timeValue: number, timeUnit: string | null, budget: number, - endPurpose: string + endPurpose: string, ): Promise => { const time = timeValue && timeUnit ? `${timeValue} ${timeUnit}` : ""; return fetchApi("/v1/generate/idea", { @@ -66,7 +68,7 @@ export const getAllGuides = async (): Promise => { return fetchApi("/v1/guide"); }; -export const saveShareLinkData = async (projectDetails: object, projectImage: object, explanation: string): Promise => { +export const saveShareLinkData = async (projectDetails: ProjectLocationState | null, projectImage: RelatedImages | null, explanation: string | null): Promise => { return fetchApi("/v1/share", { method: "POST", body: { projectDetails, projectImage, explanation }, diff --git a/client-next/lib/utils.ts b/client/lib/utils.ts similarity index 100% rename from client-next/lib/utils.ts rename to client/lib/utils.ts diff --git a/client/next.config.js b/client/next.config.js new file mode 100644 index 0000000..7833e51 --- /dev/null +++ b/client/next.config.js @@ -0,0 +1,8 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + images: { + domains: ["images.unsplash.com", "via.placeholder.com"], + }, +}; + +module.exports = nextConfig; diff --git a/client/package.json b/client/package.json index 1ea90e9..8eee50f 100644 --- a/client/package.json +++ b/client/package.json @@ -1,52 +1,57 @@ { - "name": "make-me-project", + "name": "client", + "version": "0.1.0", "private": true, - "version": "0.0.0", - "type": "module", "scripts": { - "dev": "vite", - "build": "tsc && vite build", - "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", - "lint:fix": "eslint \"src/**/*.{ts,tsx}\" --quiet --fix", - "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css}\"", - "preview": "vite preview" + "dev": "next dev -p 8080", + "build": "next build", + "start": "next start", + "lint": "next lint", + "lint:fix": "next lint --fix", + "format": "prettier --write \"./**/*.{ts,tsx,js,jsx,json,css}\"" }, "dependencies": { - "@chakra-ui/icons": "^2.1.1", - "@chakra-ui/react": "^2.8.1", - "@emotion/react": "^11.11.1", - "@emotion/styled": "^11.11.0", - "@types/react-router-dom": "^5.3.3", - "framer-motion": "^10.16.4", - "html2canvas": "^1.4.1", - "jspdf": "^2.5.1", + "@radix-ui/react-accordion": "^1.1.2", + "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-label": "^2.0.2", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-toast": "^1.1.5", + "@radix-ui/react-tooltip": "^1.0.7", + "@tailwindcss/typography": "^0.5.10", + "class-variance-authority": "^0.7.0", + "clsx": "^2.0.0", + "lucide-react": "^0.284.0", "markdown-to-jsx": "^7.3.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "react-error-boundary": "^4.0.11", - "react-icons": "^4.11.0", - "react-router-dom": "^6.15.0", - "react-syntax-highlighter": "^15.5.0" + "next": "13.5.4", + "next-themes": "^0.2.1", + "react": "^18", + "react-dom": "^18", + "react-markdown": "^9.0.0", + "tailwind-merge": "^1.14.0", + "tailwindcss-animate": "^1.0.7" }, "devDependencies": { - "@types/node": "^20.6.0", - "@types/react": "^18.2.15", - "@types/react-dom": "^18.2.7", - "@types/react-syntax-highlighter": "^15.5.7", - "@types/simplemde": "^1.11.8", - "@typescript-eslint/eslint-plugin": "^6.7.0", - "@typescript-eslint/parser": "^6.7.0", - "@vitejs/plugin-react": "^4.0.3", - "eslint": "^8.49.0", + "@types/node": "^20", + "@types/react": "^18", + "@types/react-dom": "^18", + "@typescript-eslint/eslint-plugin": "^6.7.4", + "@typescript-eslint/parser": "^6.7.4", + "autoprefixer": "^10", + "eslint": "^8", + "eslint-config-next": "13.5.4", "eslint-config-prettier": "^9.0.0", "eslint-plugin-import": "^2.28.1", - "eslint-plugin-prettier": "^5.0.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.3", + "eslint-plugin-tailwindcss": "^3.13.0", "eslint-plugin-unused-imports": "^3.0.0", + "postcss": "^8", "prettier": "^3.0.3", - "typescript": "^5.0.2", - "vite": "^4.4.5" + "tailwindcss": "^3", + "typescript": "^5" }, "prettier": { "semi": true, diff --git a/client-next/postcss.config.js b/client/postcss.config.js similarity index 96% rename from client-next/postcss.config.js rename to client/postcss.config.js index 33ad091..12a703d 100644 --- a/client-next/postcss.config.js +++ b/client/postcss.config.js @@ -3,4 +3,4 @@ module.exports = { tailwindcss: {}, autoprefixer: {}, }, -} +}; diff --git a/client-next/public/android-chrome-192x192.png b/client/public/android-chrome-192x192.png similarity index 100% rename from client-next/public/android-chrome-192x192.png rename to client/public/android-chrome-192x192.png diff --git a/client-next/public/android-chrome-512x512.png b/client/public/android-chrome-512x512.png similarity index 100% rename from client-next/public/android-chrome-512x512.png rename to client/public/android-chrome-512x512.png diff --git a/client-next/public/apple-touch-icon.png b/client/public/apple-touch-icon.png similarity index 100% rename from client-next/public/apple-touch-icon.png rename to client/public/apple-touch-icon.png diff --git a/client-next/public/favicon-16x16.png b/client/public/favicon-16x16.png similarity index 100% rename from client-next/public/favicon-16x16.png rename to client/public/favicon-16x16.png diff --git a/client-next/public/favicon-32x32.png b/client/public/favicon-32x32.png similarity index 100% rename from client-next/public/favicon-32x32.png rename to client/public/favicon-32x32.png diff --git a/client/public/image/icon.svg b/client/public/image/icon.svg deleted file mode 100644 index f5d9fe3..0000000 --- a/client/public/image/icon.svg +++ /dev/null @@ -1,117 +0,0 @@ - - - - - - - - - - - - - - diff --git a/client/public/robots.txt b/client/public/robots.txt deleted file mode 100644 index 6f27bb6..0000000 --- a/client/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx deleted file mode 100644 index 6bf51cb..0000000 --- a/client/src/App.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { Route, Routes } from "react-router-dom"; -import { ErrorBoundary } from "react-error-boundary"; -import Home from "./pages/Home"; -import ErrorFallback from "./components/ErrorFallback"; -import { Box, CSSReset } from "@chakra-ui/react"; -import Navbar from "./components/Navbar"; -import Footer from "./components/Footer"; -import FAQ from "./pages/FAQ"; -import HowToGuideDetails from "./pages/HowToGuideDetail"; -import HowToGuidesList from "./pages/HowToGuideList"; -import ProjectDetail from "./pages/ProjectDetail"; -import ProjectDetailById from "./pages/ProjectDetailById"; - -function App() { - return ( - - - - -
- - } /> - } /> - } /> - } /> - } /> - } /> - -
-
-