87 | >(({ className, ...props }, ref) => (
88 | [role=checkbox]]:translate-y-[2px]",
92 | className
93 | )}
94 | {...props}
95 | />
96 | ))
97 | TableCell.displayName = "TableCell"
98 |
99 | const TableCaption = React.forwardRef<
100 | HTMLTableCaptionElement,
101 | React.HTMLAttributes
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | TableCaption.displayName = "TableCaption"
110 |
111 | export {
112 | Table,
113 | TableHeader,
114 | TableBody,
115 | TableFooter,
116 | TableHead,
117 | TableRow,
118 | TableCell,
119 | TableCaption,
120 | }
121 |
--------------------------------------------------------------------------------
/components/ui/toast.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import * as ToastPrimitives from "@radix-ui/react-toast"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 | import { X } from "lucide-react"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const ToastProvider = ToastPrimitives.Provider
9 |
10 | const ToastViewport = React.forwardRef<
11 | React.ElementRef,
12 | React.ComponentPropsWithoutRef
13 | >(({ className, ...props }, ref) => (
14 |
22 | ))
23 | ToastViewport.displayName = ToastPrimitives.Viewport.displayName
24 |
25 | const toastVariants = cva(
26 | "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",
27 | {
28 | variants: {
29 | variant: {
30 | default: "border bg-background",
31 | destructive:
32 | "destructive group border-destructive bg-destructive text-destructive-foreground",
33 | },
34 | },
35 | defaultVariants: {
36 | variant: "default",
37 | },
38 | }
39 | )
40 |
41 | const Toast = React.forwardRef<
42 | React.ElementRef,
43 | React.ComponentPropsWithoutRef &
44 | VariantProps
45 | >(({ className, variant, ...props }, ref) => {
46 | return (
47 |
52 | )
53 | })
54 | Toast.displayName = ToastPrimitives.Root.displayName
55 |
56 | const ToastAction = React.forwardRef<
57 | React.ElementRef,
58 | React.ComponentPropsWithoutRef
59 | >(({ className, ...props }, ref) => (
60 |
68 | ))
69 | ToastAction.displayName = ToastPrimitives.Action.displayName
70 |
71 | const ToastClose = React.forwardRef<
72 | React.ElementRef,
73 | React.ComponentPropsWithoutRef
74 | >(({ className, ...props }, ref) => (
75 |
84 |
85 |
86 | ))
87 | ToastClose.displayName = ToastPrimitives.Close.displayName
88 |
89 | const ToastTitle = React.forwardRef<
90 | React.ElementRef,
91 | React.ComponentPropsWithoutRef
92 | >(({ className, ...props }, ref) => (
93 |
98 | ))
99 | ToastTitle.displayName = ToastPrimitives.Title.displayName
100 |
101 | const ToastDescription = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | ToastDescription.displayName = ToastPrimitives.Description.displayName
112 |
113 | type ToastProps = React.ComponentPropsWithoutRef
114 |
115 | type ToastActionElement = React.ReactElement
116 |
117 | export {
118 | type ToastProps,
119 | type ToastActionElement,
120 | ToastProvider,
121 | ToastViewport,
122 | Toast,
123 | ToastTitle,
124 | ToastDescription,
125 | ToastClose,
126 | ToastAction,
127 | }
128 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | // Inspired by react-hot-toast library
2 | import * as React from "react"
3 |
4 | import type { ToastActionElement, ToastProps } from "@/components/ui/toast"
5 |
6 | const TOAST_LIMIT = 1
7 | const TOAST_REMOVE_DELAY = 1000000
8 |
9 | type ToasterToast = ToastProps & {
10 | id: string
11 | title?: React.ReactNode
12 | description?: React.ReactNode
13 | action?: ToastActionElement
14 | }
15 |
16 | const actionTypes = {
17 | ADD_TOAST: "ADD_TOAST",
18 | UPDATE_TOAST: "UPDATE_TOAST",
19 | DISMISS_TOAST: "DISMISS_TOAST",
20 | REMOVE_TOAST: "REMOVE_TOAST",
21 | } as const
22 |
23 | let count = 0
24 |
25 | function genId() {
26 | count = (count + 1) % Number.MAX_VALUE
27 | return count.toString()
28 | }
29 |
30 | type ActionType = typeof actionTypes
31 |
32 | type Action =
33 | | {
34 | type: ActionType["ADD_TOAST"]
35 | toast: ToasterToast
36 | }
37 | | {
38 | type: ActionType["UPDATE_TOAST"]
39 | toast: Partial
40 | }
41 | | {
42 | type: ActionType["DISMISS_TOAST"]
43 | toastId?: ToasterToast["id"]
44 | }
45 | | {
46 | type: ActionType["REMOVE_TOAST"]
47 | toastId?: ToasterToast["id"]
48 | }
49 |
50 | interface State {
51 | toasts: ToasterToast[]
52 | }
53 |
54 | const toastTimeouts = new Map>()
55 |
56 | const addToRemoveQueue = (toastId: string) => {
57 | if (toastTimeouts.has(toastId)) {
58 | return
59 | }
60 |
61 | const timeout = setTimeout(() => {
62 | toastTimeouts.delete(toastId)
63 | dispatch({
64 | type: "REMOVE_TOAST",
65 | toastId: toastId,
66 | })
67 | }, TOAST_REMOVE_DELAY)
68 |
69 | toastTimeouts.set(toastId, timeout)
70 | }
71 |
72 | export const reducer = (state: State, action: Action): State => {
73 | switch (action.type) {
74 | case "ADD_TOAST":
75 | return {
76 | ...state,
77 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
78 | }
79 |
80 | case "UPDATE_TOAST":
81 | return {
82 | ...state,
83 | toasts: state.toasts.map((t) =>
84 | t.id === action.toast.id ? { ...t, ...action.toast } : t
85 | ),
86 | }
87 |
88 | case "DISMISS_TOAST": {
89 | const { toastId } = action
90 |
91 | // ! Side effects ! - This could be extracted into a dismissToast() action,
92 | // but I'll keep it here for simplicity
93 | if (toastId) {
94 | addToRemoveQueue(toastId)
95 | } else {
96 | state.toasts.forEach((toast) => {
97 | addToRemoveQueue(toast.id)
98 | })
99 | }
100 |
101 | return {
102 | ...state,
103 | toasts: state.toasts.map((t) =>
104 | t.id === toastId || toastId === undefined
105 | ? {
106 | ...t,
107 | open: false,
108 | }
109 | : t
110 | ),
111 | }
112 | }
113 | case "REMOVE_TOAST":
114 | if (action.toastId === undefined) {
115 | return {
116 | ...state,
117 | toasts: [],
118 | }
119 | }
120 | return {
121 | ...state,
122 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
123 | }
124 | }
125 | }
126 |
127 | const listeners: Array<(state: State) => void> = []
128 |
129 | let memoryState: State = { toasts: [] }
130 |
131 | function dispatch(action: Action) {
132 | memoryState = reducer(memoryState, action)
133 | listeners.forEach((listener) => {
134 | listener(memoryState)
135 | })
136 | }
137 |
138 | type Toast = Omit
139 |
140 | function toast({ ...props }: Toast) {
141 | const id = genId()
142 |
143 | const update = (props: ToasterToast) =>
144 | dispatch({
145 | type: "UPDATE_TOAST",
146 | toast: { ...props, id },
147 | })
148 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
149 |
150 | dispatch({
151 | type: "ADD_TOAST",
152 | toast: {
153 | ...props,
154 | id,
155 | open: true,
156 | onOpenChange: (open) => {
157 | if (!open) dismiss()
158 | },
159 | },
160 | })
161 |
162 | return {
163 | id: id,
164 | dismiss,
165 | update,
166 | }
167 | }
168 |
169 | function useToast() {
170 | const [state, setState] = React.useState(memoryState)
171 |
172 | React.useEffect(() => {
173 | listeners.push(setState)
174 | return () => {
175 | const index = listeners.indexOf(setState)
176 | if (index > -1) {
177 | listeners.splice(index, 1)
178 | }
179 | }
180 | }, [state])
181 |
182 | return {
183 | ...state,
184 | toast,
185 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
186 | }
187 | }
188 |
189 | export { useToast, toast }
190 |
--------------------------------------------------------------------------------
/config/site.ts:
--------------------------------------------------------------------------------
1 | export type SiteConfig = typeof siteConfig
2 |
3 | export const siteConfig = {
4 | name: "Chef Genie",
5 | url: "https://chef-genie.app",
6 | ogImage: "https://chef-genie.app/og.png",
7 | description: "An open-source recipe generator powered by OpenAi and ChatGPT.",
8 | mainNav: [
9 | {
10 | title: "Chef Genie Homepage",
11 | href: "/",
12 | },
13 | ],
14 | links: {
15 | twitter: "https://twitter.com/faultyled",
16 | github: "https://github.com/giacomogaglione",
17 | docs: "https://chef-genie.app",
18 | },
19 | }
20 |
--------------------------------------------------------------------------------
/lib/actions.ts:
--------------------------------------------------------------------------------
1 | "use server"
2 |
3 | import { revalidatePath } from "next/cache"
4 | import { auth } from "@clerk/nextjs"
5 |
6 | import { supabaseClient, supabaseClientPublic } from "@/lib/supabase-client"
7 |
8 | async function getSupabaseClient() {
9 | const { getToken } = auth()
10 | const supabaseAccessToken = await getToken({ template: "chef-genie" })
11 | return await supabaseClient(supabaseAccessToken as string)
12 | }
13 |
14 | export async function saveGeneration(generatedRecipe) {
15 | const supabase = await supabaseClientPublic()
16 |
17 | const data = {
18 | content_json: generatedRecipe,
19 | title: generatedRecipe.title,
20 | difficulty: generatedRecipe.difficulty,
21 | cooking_time: generatedRecipe.cooking_time,
22 | people: generatedRecipe.people,
23 | low_calories: generatedRecipe.low_calori,
24 | vegan: generatedRecipe.vegan,
25 | paleo: generatedRecipe.paleo,
26 | description: generatedRecipe.description,
27 | calories: generatedRecipe.calories,
28 | proteins: generatedRecipe.macros.protein,
29 | fats: generatedRecipe.macros.fats,
30 | carbs: generatedRecipe.macros.carbs,
31 | }
32 |
33 | await supabase.from("generations").insert([data])
34 |
35 | revalidatePath("/")
36 | }
37 |
38 | export async function saveRecipe(generatedRecipe) {
39 | const supabase = await getSupabaseClient()
40 | const userId = auth().userId
41 |
42 | if (!userId) throw new Error("User ID not found")
43 |
44 | const data = {
45 | user_id: userId,
46 | title: generatedRecipe.title,
47 | description: generatedRecipe.description,
48 | content_json: generatedRecipe,
49 | ingredients: generatedRecipe.ingredients,
50 | difficulty: generatedRecipe.difficulty,
51 | cooking_time: generatedRecipe.cooking_time,
52 | people: generatedRecipe.people,
53 | low_calories: generatedRecipe.low_calori,
54 | vegan: generatedRecipe.vegan,
55 | paleo: generatedRecipe.paleo,
56 | calories: generatedRecipe.calories,
57 | proteins: generatedRecipe.macros.protein,
58 | fats: generatedRecipe.macros.fats,
59 | carbs: generatedRecipe.macros.carbs,
60 | }
61 | try {
62 | await supabase.from("recipes").insert([data])
63 | } catch (error) {
64 | throw new Error("Failed to save the recipe.")
65 | }
66 | }
67 |
68 | export async function deleteRecipe(id: string) {
69 | const supabase = await getSupabaseClient()
70 | const userId = auth().userId
71 |
72 | if (!userId) throw new Error("User ID not found")
73 |
74 | await supabase.from("recipes").delete().eq("id", id)
75 |
76 | revalidatePath("/dashboard/my-recipes")
77 | }
78 |
--------------------------------------------------------------------------------
/lib/fonts.ts:
--------------------------------------------------------------------------------
1 | import { Inter as FontSans } from "next/font/google"
2 |
3 | export const fontSans = FontSans({
4 | subsets: ["latin"],
5 | variable: "--font-sans",
6 | })
7 |
--------------------------------------------------------------------------------
/lib/generate-prompt.ts:
--------------------------------------------------------------------------------
1 | import { FormData } from "@/types/types"
2 |
3 | export function generatePrompt(values: FormData): string {
4 | const dietRestrictions = `
5 | - Low-calorie: ${values.low_calori ? "Yes" : "No"}
6 | - Vegan: ${values.vegan ? "Yes" : "No"}
7 | - Paleo: ${values.paleo ? "Yes" : "No"}
8 | `
9 | return `
10 | You are an expert culinary chef who has cooked for the best restaurants in the world.
11 | Craft a delightful, creative and unique recipe with the following considerations:
12 |
13 | Rules:
14 | - Response must be in JSON format.
15 | - Recipe must have a creative Title.
16 | - Include detailed instructions with estimated cooking times for each step.
17 | - Adhere to the following dietary preferences: ${dietRestrictions}
18 | - Utilize only the available ingredients (${values.ingredients}).
19 | Avoid incompatible ingredients based on the specified diet.
20 | - Ensure the cooking time is under ${values.cooking_time} minutes.
21 | - Design the recipe to serve ${values.people} people.
22 | - Evaluate the difficulty of execution as ${values.difficulty}.
23 | - Recipe must have a short description.
24 | - Be creative with the cooking techniques and flavor combinations
25 | - Feel free to incorporate herbs and spices for an extra burst of flavor
26 |
27 |
28 | The JSON object must include the following fields:
29 | - "title": [string]
30 | - "description": [string]
31 | - "people": [number] (based on the provided input)
32 | - "difficulty": [string] (based on the provided input)
33 | - "cooking_time": [number] (based on the provided input)
34 | - "low_calori": [string] (based on the provided input)
35 | - "vegan": [string] (based on the provided input)
36 | - "paleo": [string] (based on the provided input)
37 | - "calories": [number],
38 | - "macros": {"protein": [number], "fats": [number], "carbs": [number]},
39 | - "ingredients": [{"name": [string], "amount": [string]}, ...] (based on the provided diet type and ingredients provided),
40 | - "instructions": [{"step": [number], "description": [string]}, ...]
41 |
42 |
43 | Format the response as a valid JSON object with all fields filled. Here is the structure for reference:
44 |
45 | {
46 | "title": /* details */,
47 | "description": /* details */,
48 | "people": /* details */,
49 | "difficulty": /* details */,
50 | "cooking_time": /* details */,
51 | "low_calori": /* details */,
52 | "vegan": /* details */,
53 | "paleo": /* details */,
54 | "calories": /* details */,
55 | "macros": { /* details */ },
56 | "ingredients": { /* details */ },
57 | "instructions": { /* details */ }
58 | }
59 |
60 | Respond only with the completed JSON object, without any additional explanatory or descriptive text. The JSON should be complete and ready for parsing
61 |
62 | `
63 | }
64 |
--------------------------------------------------------------------------------
/lib/supabase-client.ts:
--------------------------------------------------------------------------------
1 | import { createClient } from "@supabase/supabase-js"
2 |
3 | const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL
4 | const supabaseKey = process.env.NEXT_PUBLIC_SUPABASE_KEY
5 |
6 | export const supabaseClient = async (supabaseAccessToken: string) => {
7 | const supabase = createClient(
8 | supabaseUrl as string,
9 | supabaseKey as string,
10 | {
11 | global: { headers: { Authorization: `Bearer ${supabaseAccessToken}` } },
12 | }
13 | )
14 |
15 | return supabase
16 | }
17 |
18 | export const supabaseClientPublic = async () => {
19 | const supabase = createClient(
20 | supabaseUrl as string,
21 | supabaseKey as string
22 | )
23 |
24 | return supabase
25 | }
26 |
--------------------------------------------------------------------------------
/lib/supabase-queries.ts:
--------------------------------------------------------------------------------
1 | import { supabaseClient, supabaseClientPublic } from "@/lib/supabase-client"
2 |
3 | export const getRecipesByUserId = async (userId, supabaseAccessToken) => {
4 | const supabase = await supabaseClient(supabaseAccessToken as string)
5 | const { data: recipes } = await supabase
6 | .from("recipes")
7 | .select()
8 | .eq("user_id", userId)
9 | .order("created_at", { ascending: false })
10 |
11 | return recipes
12 | }
13 |
14 | export async function getLatestRecipes() {
15 | const supabase = await supabaseClientPublic()
16 | try {
17 | const { data: recipes } = await supabase
18 | .from("generations")
19 | .select()
20 | .range(0, 2)
21 | .order("created_at", { ascending: false })
22 |
23 | return recipes
24 | } catch (error) {
25 | console.error("Error:", error)
26 | return null
27 | }
28 | }
29 |
30 | export async function getRecipe(id: string) {
31 | const supabase = await supabaseClientPublic()
32 | try {
33 | const { data: recipe } = await supabase
34 | .from("recipes")
35 | .select("content_json")
36 | .eq("id", id)
37 | .single()
38 |
39 | return recipe
40 | } catch (error) {
41 | console.error("Error:", error)
42 | return null
43 | }
44 | }
45 |
46 | export async function getRecipesCount() {
47 | const supabase = await supabaseClientPublic()
48 | try {
49 | const { count } = await supabase
50 | .from("generations")
51 | .select("*", { count: "exact", head: true })
52 |
53 | return count
54 | } catch (error) {
55 | console.error("Error:", error)
56 | return null
57 | }
58 | }
59 |
60 | export async function getRecipePublic(id: string) {
61 | const supabase = await supabaseClientPublic()
62 | try {
63 | const { data: recipe } = await supabase
64 | .from("generations")
65 | .select("content_json")
66 | .eq("id", id)
67 | .single()
68 |
69 | return recipe ? recipe.content_json : null
70 | } catch (error) {
71 | console.error("Error:", error)
72 | return null
73 | }
74 | }
75 |
76 | export async function getRecipePrivate(id: string, supabaseAccessToken) {
77 | const supabase = await supabaseClient(supabaseAccessToken as string)
78 | try {
79 | const { data: recipe } = await supabase
80 | .from("recipes")
81 | .select("content_json")
82 | .eq("id", id)
83 | .single()
84 |
85 | return recipe ? recipe.content_json : null
86 | } catch (error) {
87 | console.error("Error:", error)
88 | return null
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { clsx, type ClassValue } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from "next/server"
2 | import { authMiddleware } from "@clerk/nextjs"
3 |
4 | export default authMiddleware({
5 | publicRoutes: [
6 | "/",
7 | "/sign-in(.*)",
8 | "/sign-up(.*)",
9 | "/dashboard(.*)",
10 | "/dashboard",
11 | "/sign-out",
12 | "/api(.*)",
13 | "/recipes(.*)",
14 | ],
15 | async afterAuth(auth, req) {
16 | if (auth.isPublicRoute) {
17 | // For public routes, we don't need to do anything
18 | return NextResponse.next()
19 | }
20 |
21 | const url = new URL(req.nextUrl.origin)
22 |
23 | if (!auth.userId) {
24 | // If user tries to access a private route without being authenticated,
25 | // redirect them to the sign in page
26 | url.pathname = "/sign-in"
27 | return NextResponse.redirect(url)
28 | }
29 | },
30 | })
31 |
32 | export const config = {
33 | matcher: ["/((?!.*\\..*|_next).*)", "/", "/(api|trpc)(.*)"],
34 | }
35 |
--------------------------------------------------------------------------------
/next-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | ///
3 |
4 | // NOTE: This file should not be edited
5 | // see https://nextjs.org/docs/basic-features/typescript for more information.
6 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | reactStrictMode: true,
4 | }
5 |
6 | export default nextConfig
7 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chef-gpt",
3 | "version": "0.0.1",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint",
10 | "lint:fix": "next lint --fix",
11 | "preview": "next build && next start",
12 | "typecheck": "tsc --noEmit",
13 | "format:write": "prettier --write \"**/*.{ts,tsx,mdx}\" --cache",
14 | "format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache"
15 | },
16 | "dependencies": {
17 | "@clerk/nextjs": "^4.29.3",
18 | "@clerk/themes": "^1.7.9",
19 | "@hookform/resolvers": "^3.3.4",
20 | "@radix-ui/react-avatar": "^1.0.3",
21 | "@radix-ui/react-dialog": "^1.0.5",
22 | "@radix-ui/react-dropdown-menu": "^2.0.5",
23 | "@radix-ui/react-label": "^2.0.1",
24 | "@radix-ui/react-popover": "^1.0.7",
25 | "@radix-ui/react-radio-group": "^1.1.3",
26 | "@radix-ui/react-select": "^1.2.2",
27 | "@radix-ui/react-separator": "^1.0.3",
28 | "@radix-ui/react-slider": "^1.1.2",
29 | "@radix-ui/react-slot": "^1.0.2",
30 | "@radix-ui/react-switch": "^1.0.3",
31 | "@radix-ui/react-toast": "^1.1.4",
32 | "@supabase/supabase-js": "^2.36.0",
33 | "@tanstack/react-table": "^8.11.8",
34 | "@vercel/analytics": "^1.1.1",
35 | "@vercel/speed-insights": "^1.0.2",
36 | "ai": "^2.2.31",
37 | "class-variance-authority": "^0.7.0",
38 | "clsx": "^2.1.0",
39 | "cmdk": "^0.2.1",
40 | "lucide-react": "^0.279.0",
41 | "next": "^14.0.4",
42 | "next-themes": "^0.2.1",
43 | "openai": "^4.24.7",
44 | "react": "^18.2.0",
45 | "react-dom": "^18.2.0",
46 | "react-hook-form": "^7.49.3",
47 | "recharts": "^2.10.4",
48 | "sonner": "^1.4.0",
49 | "supabase": "^1.142.0",
50 | "tailwind-merge": "^2.2.1",
51 | "tailwindcss-animate": "^1.0.7",
52 | "zod": "^3.22.4"
53 | },
54 | "devDependencies": {
55 | "@ianvs/prettier-plugin-sort-imports": "^4.1.1",
56 | "@types/node": "^20.11.0",
57 | "@types/react": "^18.2.8",
58 | "@types/react-dom": "^18.2.18",
59 | "@typescript-eslint/parser": "^6.20.0",
60 | "autoprefixer": "^10.4.13",
61 | "encoding": "^0.1.13",
62 | "eslint": "^8.31.0",
63 | "eslint-config-next": "^14.1.0",
64 | "eslint-config-prettier": "^9.1.0",
65 | "eslint-plugin-react": "^7.31.11",
66 | "eslint-plugin-tailwindcss": "^3.14.1",
67 | "postcss": "^8.4.31",
68 | "prettier": "^3.2.4",
69 | "tailwindcss": "^3.4.1",
70 | "typescript": "^5.2.2"
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/prettier.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('prettier').Config} */
2 | module.exports = {
3 | endOfLine: "lf",
4 | semi: false,
5 | singleQuote: false,
6 | tabWidth: 2,
7 | trailingComma: "es5",
8 | importOrder: [
9 | "^(react/(.*)$)|^(react$)",
10 | "^(next/(.*)$)|^(next$)",
11 | "",
12 | "",
13 | "^types$",
14 | "^@/types/(.*)$",
15 | "^@/config/(.*)$",
16 | "^@/lib/(.*)$",
17 | "^@/hooks/(.*)$",
18 | "^@/components/ui/(.*)$",
19 | "^@/components/(.*)$",
20 | "^@/styles/(.*)$",
21 | "^@/app/(.*)$",
22 | "",
23 | "^[./]",
24 | ],
25 | importOrderSeparation: false,
26 | importOrderSortSpecifiers: true,
27 | importOrderBuiltinModulesToTop: true,
28 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"],
29 | importOrderMergeDuplicateImports: true,
30 | importOrderCombineTypeAndValueImports: true,
31 | plugins: ["@ianvs/prettier-plugin-sort-imports"],
32 | }
33 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/giacomogaglione/chef-gpt/7d37001b8d6a97ca3e7219af7e7bf5f2dc811d5c/public/favicon.ico
--------------------------------------------------------------------------------
/public/og.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/giacomogaglione/chef-gpt/7d37001b8d6a97ca3e7219af7e7bf5f2dc811d5c/public/og.png
--------------------------------------------------------------------------------
/styles/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | @layer base {
6 | :root {
7 | --background: 0 0% 100%;
8 | --foreground: 240 10% 3.9%;
9 | --card: 0 0% 100%;
10 | --card-foreground: 240 10% 3.9%;
11 | --popover: 0 0% 100%;
12 | --popover-foreground: 240 10% 3.9%;
13 | --primary: 240 5.9% 10%;
14 | --primary-foreground: 0 0% 98%;
15 | --secondary: 240 4.8% 95.9%;
16 | --secondary-foreground: 240 5.9% 10%;
17 | --muted: 240 4.8% 95.9%;
18 | --muted-foreground: 240 3.8% 46.1%;
19 | --accent: 240 4.8% 95.9%;
20 | --accent-foreground: 240 5.9% 10%;
21 | --destructive: 0 72.22% 50.59%;
22 | --destructive-foreground: 0 0% 98%;
23 | --border: 240 5.9% 90%;
24 | --input: 240 5.9% 90%;
25 | --ring: 240 5% 64.9%;
26 | --radius: 0.5rem;
27 | --vegan: 167.23 85.45% 89.22%;
28 | --paleo: 48 96.49% 88.82%;
29 | }
30 |
31 | .dark {
32 | --background: 240 10% 3.9%;
33 | --foreground: 0 0% 98%;
34 | --card: 240 10% 3.9%;
35 | --card-foreground: 0 0% 98%;
36 | --popover: 240 10% 3.9%;
37 | --popover-foreground: 0 0% 98%;
38 | --primary: 0 0% 98%;
39 | --primary-foreground: 240 5.9% 10%;
40 | --secondary: 240 3.7% 15.9%;
41 | --secondary-foreground: 0 0% 98%;
42 | --muted: 240 3.7% 15.9%;
43 | --muted-foreground: 240 5% 64.9%;
44 | --accent: 240 3.7% 15.9%;
45 | --accent-foreground: 0 0% 98%;
46 | --destructive: 0 62.8% 30.6%;
47 | --destructive-foreground: 0 85.7% 97.3%;
48 | --border: 240 3.7% 15.9%;
49 | --input: 240 3.7% 15.9%;
50 | --ring: 240 4.9% 83.9%;
51 | --vegan: 164.17 85.71% 16.47%;
52 | --paleo: 21.71 77.78% 26.47%;
53 | }
54 | }
55 |
56 | @layer base {
57 | * {
58 | @apply border-border;
59 | }
60 | body {
61 | @apply bg-background text-foreground;
62 | }
63 | }
64 |
65 | .cl-card {
66 | @apply bg-popover text-foreground;
67 | }
68 |
69 | .cl-headerTitle {
70 | @apply text-foreground;
71 | }
72 |
73 | .cl-headerSubtitle,
74 | .cl-userButtonPopoverActionButtonText,
75 | .cl-dividerText,
76 | .cl-footerActionText {
77 | @apply text-muted-foreground;
78 | }
79 |
80 | .cl-dividerLine {
81 | @apply bg-muted;
82 | }
83 |
84 | .cl-socialButtonsBlockButton {
85 | @apply text-muted-foreground border-input;
86 | }
87 |
88 | .cl-formFieldLabel {
89 | @apply text-foreground;
90 | }
91 |
92 | .cl-formFieldInput {
93 | @apply bg-transparent text-foreground border-input;
94 | }
95 |
96 | .cl-page {
97 | @apply text-foreground;
98 | }
99 |
100 | .cl-profileSectionTitleText, .cl-userPreview, .cl-accordionTriggerButton {
101 | @apply text-foreground;
102 | }
103 |
104 | @layer components {
105 | .skeleton {
106 | @apply relative before:animate-shimmer before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-black/[0.08] before:to-transparent before:z-50 overflow-hidden;
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const { fontFamily } = require("tailwindcss/defaultTheme")
2 |
3 | /** @type {import('tailwindcss').Config} */
4 | module.exports = {
5 | darkMode: ["class"],
6 | content: [
7 | './pages/**/*.{ts,tsx}',
8 | './components/**/*.{ts,tsx}',
9 | './app/**/*.{ts,tsx}',
10 | './src/**/*.{ts,tsx}',
11 | ],
12 | theme: {
13 | container: {
14 | center: true,
15 | padding: "2rem",
16 | screens: {
17 | "2xl": "1400px",
18 | },
19 | },
20 | extend: {
21 | fontFamily: {
22 | sans: ["var(--font-sans)", ...fontFamily.sans],
23 | },
24 | colors: {
25 | border: "hsl(var(--border))",
26 | input: "hsl(var(--input))",
27 | ring: "hsl(var(--ring))",
28 | background: "hsl(var(--background))",
29 | foreground: "hsl(var(--foreground))",
30 | vegan: "hsl(var(--vegan))",
31 | paleo: "hsl(var(--paleo))",
32 | primary: {
33 | DEFAULT: "hsl(var(--primary))",
34 | foreground: "hsl(var(--primary-foreground))",
35 | },
36 | secondary: {
37 | DEFAULT: "hsl(var(--secondary))",
38 | foreground: "hsl(var(--secondary-foreground))",
39 | },
40 | destructive: {
41 | DEFAULT: "hsl(var(--destructive))",
42 | foreground: "hsl(var(--destructive-foreground))",
43 | },
44 | muted: {
45 | DEFAULT: "hsl(var(--muted))",
46 | foreground: "hsl(var(--muted-foreground))",
47 | },
48 | accent: {
49 | DEFAULT: "hsl(var(--accent))",
50 | foreground: "hsl(var(--accent-foreground))",
51 | },
52 | popover: {
53 | DEFAULT: "hsl(var(--popover))",
54 | foreground: "hsl(var(--popover-foreground))",
55 | },
56 | card: {
57 | DEFAULT: "hsl(var(--card))",
58 | foreground: "hsl(var(--card-foreground))",
59 | },
60 | },
61 | borderRadius: {
62 | lg: "var(--radius)",
63 | md: "calc(var(--radius) - 2px)",
64 | sm: "calc(var(--radius) - 4px)",
65 | },
66 | keyframes: {
67 | "accordion-down": {
68 | from: { height: 0 },
69 | to: { height: "var(--radix-accordion-content-height)" },
70 | },
71 | "accordion-up": {
72 | from: { height: "var(--radix-accordion-content-height)" },
73 | to: { height: 0 },
74 | },
75 | shimmer: {
76 | "0%": {
77 | transform: "translateX(-100%)",
78 | },
79 | "100%": {
80 | transform: "translateX(100%)",
81 | },
82 | },
83 | },
84 | animation: {
85 | "accordion-down": "accordion-down 0.2s ease-out",
86 | "accordion-up": "accordion-up 0.2s ease-out",
87 | shimmer: "shimmer 2s ease-in-out infinite",
88 | },
89 | },
90 | },
91 | plugins: [require("tailwindcss-animate")],
92 | }
93 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": false,
7 | "forceConsistentCasingInFileNames": true,
8 | "noEmit": true,
9 | "incremental": true,
10 | "esModuleInterop": true,
11 | "module": "esnext",
12 | "moduleResolution": "node",
13 | "resolveJsonModule": true,
14 | "isolatedModules": true,
15 | "jsx": "preserve",
16 | "baseUrl": ".",
17 | "paths": {
18 | "@/*": ["./*"]
19 | },
20 | "plugins": [
21 | {
22 | "name": "next"
23 | }
24 | ],
25 | "strictNullChecks": true
26 | },
27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
28 | "exclude": ["node_modules"]
29 | }
30 |
--------------------------------------------------------------------------------
/types/database.types.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/giacomogaglione/chef-gpt/7d37001b8d6a97ca3e7219af7e7bf5f2dc811d5c/types/database.types.ts
--------------------------------------------------------------------------------
/types/types.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod"
2 |
3 | export interface NavItem {
4 | title: string
5 | href?: string
6 | disabled?: boolean
7 | external?: boolean
8 | }
9 |
10 | export const formSchema = z.object({
11 | ingredients: z.string().min(2, {
12 | message: "Please add at least one ingredient",
13 | }),
14 | cooking_time: z.array(z.number()).optional(),
15 | people: z.enum(["2", "4", "6"]).optional(),
16 | difficulty: z.string().optional(),
17 | low_calori: z.boolean().default(false).optional(),
18 | vegan: z.boolean().default(false).optional(),
19 | paleo: z.boolean().default(false).optional(),
20 | })
21 |
22 | export const defaultValues: FormData = {
23 | ingredients: "",
24 | cooking_time: [15],
25 | people: "2",
26 | difficulty: "Easy",
27 | low_calori: true,
28 | vegan: false,
29 | paleo: false,
30 | }
31 |
32 | export type FormData = z.infer
33 |
34 | export interface Recipe {
35 | title: string
36 | description: string
37 | cooking_time: number
38 | calories: number
39 | difficulty: string
40 | macros: {
41 | protein: number
42 | fats: number
43 | carbs: number
44 | }
45 | ingredients: Array<{ name: string; amount: number | string }>
46 | instructions: Array<{ step: number; description: string | string }>
47 | }
48 |
--------------------------------------------------------------------------------
|