Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions fullstack/fullstack/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Connection String
DATABASE_URL=postgresql://postgres.uutjayiymethqofitiph:JungieGwapo1@aws-1-ap-southeast-1.pooler.supabase.com:5432/postgres

# Google OAuth (APIs & Services > Credentials)
AUTH_GOOGLE_ID=868114878763-n8np7ep4s2lbqr1uvdpc2gef44h4lam6.apps.googleusercontent.com
AUTH_GOOGLE_SECRET=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

# Secret for encrypting cookies (Generate with: npx auth secret)
NEXTAUTH_SECRET=8UlV+TUZUlt1YOC73jenB48+DGYrELOD75m4bZKjmTg=
NEXTAUTH_URL=http://localhost:3000

#Groq API Key
GROQ_API_KEY=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
Binary file added fullstack/fullstack/.gitignore
Binary file not shown.
36 changes: 36 additions & 0 deletions fullstack/fullstack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/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/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## 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/app/building-your-application/deploying) for more details.
9 changes: 9 additions & 0 deletions fullstack/fullstack/app/(todolist)/_components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export default function Footer() {
return (
<footer className="py-8 border-t border-gray-100 text-center">
<p className="text-[10px] font-bold text-gray-300 uppercase tracking-widest">
Codebility Assessment • Fullstack 3-5
</p>
</footer>
);
}
98 changes: 98 additions & 0 deletions fullstack/fullstack/app/(todolist)/_components/LoginButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"use client";
import { ChevronDown, LogIn, LogOut, User } from "lucide-react";
import { signIn, signOut, useSession } from "next-auth/react";
import Image from "next/image";
import { useState } from "react";

export default function LoginButton() {
const { data: session, status } = useSession();
const [isOpen, setIsOpen] = useState(false);

if (status === "loading") {
return (
<div className="h-10 w-10 md:w-32 animate-pulse bg-gray-100 rounded-full border border-gray-200" />
);
}

if (session) {
return (
<div className="relative">
{/* Profile Trigger - Clean White Pill */}
<button
onClick={() => setIsOpen(!isOpen)}
className="flex items-center gap-2.5 p-1 pr-4 rounded-full bg-white border border-gray-200 hover:border-gray-300 hover:bg-gray-50 transition-all active:scale-95 group shadow-sm"
>
{session.user?.image ? (
<Image
src={session.user.image}
alt="Profile"
width={32}
height={32}
className="rounded-full border border-gray-100"
/>
) : (
<div className="p-2 rounded-full bg-blue-50 text-blue-600">
<User size={16} />
</div>
)}

<span className="hidden md:block text-sm font-medium text-[#3c4043]">
{session.user?.name?.split(" ")[0]}
</span>

<ChevronDown
size={16}
className={`text-gray-400 transition-transform duration-300 ${isOpen ? "rotate-180 text-gray-700" : ""}`}
/>
</button>

{/* Dropdown Menu - Clean White Card */}
{isOpen && (
<>
<div
className="fixed inset-0 z-10"
onClick={() => setIsOpen(false)}
/>
<div className="absolute right-0 mt-3 w-64 bg-white border border-gray-200 rounded-2xl py-2 shadow-xl z-20 animate-in fade-in zoom-in-95 slide-in-from-top-2 duration-200">
<div className="px-5 py-3 border-b border-gray-100 mb-1">
<p className="text-[11px] uppercase tracking-wider text-gray-500 font-bold">
Google Account
</p>
<p className="text-sm text-[#202124] truncate font-medium mt-0.5">
{session.user?.name}
</p>
<p className="text-xs text-gray-500 truncate mt-0.5">
{session.user?.email}
</p>
</div>

<button
onClick={() => signOut()}
className="group flex w-full items-center justify-between px-5 py-3 text-sm font-medium text-gray-700 hover:bg-gray-50 transition-all"
>
<span>Sign out</span>
<LogOut
size={16}
className="text-gray-400 group-hover:text-red-500 transition-colors"
/>
</button>
</div>
</>
)}
</div>
);
}

return (
<button
onClick={() => signIn("google", { callbackUrl: "/todo" })}
className="group flex items-center gap-2.5 bg-blue-600 px-6 py-2.5 rounded-full text-white text-sm font-semibold hover:bg-blue-700 transition-all active:scale-95 shadow-md shadow-blue-600/20"
>
<span>Sign in</span>
<LogIn
size={16}
className="group-hover:translate-x-1 transition-transform"
/>
</button>
);
}
26 changes: 26 additions & 0 deletions fullstack/fullstack/app/(todolist)/_components/Navbar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Image from "next/image";
import Link from "next/link";
import LoginButton from "./LoginButton";

export default function Navbar() {
return (
<nav className="fixed top-0 w-full z-50 bg-white border-b border-gray-100 px-6 md:px-12 py-3 flex justify-between items-center shadow-sm">
<Link href="/" className="flex items-center gap-3 group">
<div className="relative w-10 h-10 flex items-center justify-center bg-blue-50 rounded-xl transition-colors group-hover:bg-blue-100">
<Image
src="/favicon.ico"
alt="Favicon | Logo"
width={28}
height={28}
priority
className="object-contain rounded-lg"
/>
</div>
</Link>

<div className="flex items-center gap-4">
<LoginButton />
</div>
</nav>
);
}
6 changes: 6 additions & 0 deletions fullstack/fullstack/app/(todolist)/_components/Providers.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"use client";
import { SessionProvider } from "next-auth/react";

export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}
154 changes: 154 additions & 0 deletions fullstack/fullstack/app/(todolist)/_components/TodoForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"use client";
import { createCookingOrder, createTodo } from "@/app/lib/services";
import {
ChefHat,
Loader2,
Lock,
UtensilsCrossed,
Plus,
Sparkles,
} from "lucide-react";
import { useRef, useTransition, useState } from "react";
import { useRouter } from "next/navigation";
import { useSession } from "next-auth/react";

export default function TodoForm() {
const formRef = useRef<HTMLFormElement>(null);
const [isPending, startTransition] = useTransition();
const [error, setError] = useState<string | null>(null);
const [isAiMode, setIsAiMode] = useState(true);
const router = useRouter();
const { status } = useSession();

const isAuthenticated = status === "authenticated";

const handleSubmit = async (formData: FormData) => {
setError(null);
if (!isAuthenticated) return setError("AUTH_REQUIRED_");

const input = formData.get("task") as string;
if (!input || input.trim().length === 0)
return setError(isAiMode ? "WHAT_ARE_WE_COOKING?" : "STEP_IS_EMPTY_");

startTransition(async () => {
const res = isAiMode
? await createCookingOrder(input)
: await createTodo(input);

if (res.success) {
formRef.current?.reset();
router.refresh();
} else {
setError(res.error || "CHEF_IS_BUSY_");
}
});
};

return (
<div className="mb-10">
<div className="flex gap-4 mb-4 ml-1">
<button
onClick={() => setIsAiMode(true)}
className={`flex items-center gap-2 px-3 py-1.5 rounded-full transition-all ${
isAiMode
? "bg-orange-100 text-orange-600 ring-1 ring-orange-200"
: "text-gray-400 hover:text-gray-600"
}`}
>
<Sparkles size={14} />
<span className="text-[10px] font-bold uppercase tracking-wider">
AI Order
</span>
</button>
<button
onClick={() => setIsAiMode(false)}
className={`flex items-center gap-2 px-3 py-1.5 rounded-full transition-all ${
!isAiMode
? "bg-blue-100 text-blue-600 ring-1 ring-blue-200"
: "text-gray-400 hover:text-gray-600"
}`}
>
<Plus size={14} />
<span className="text-[10px] font-bold uppercase tracking-wider">
Manual Step
</span>
</button>
</div>

<form
ref={formRef}
action={handleSubmit}
className={`relative transition-all duration-300 ${
isPending ? "scale-[0.99] opacity-80" : "scale-100 opacity-100"
}`}
>
<input
name="task"
type="text"
disabled={isPending || !isAuthenticated}
placeholder={
!isAuthenticated
? "Sign in to start the kitchen"
: isAiMode
? "Enter a dish (e.g. Adobo)..."
: "Add a custom preparation step..."
}
autoComplete="off"
onChange={() => error && setError(null)}
className={`w-full bg-white border rounded-2xl py-5 pl-14 pr-28 text-[15px] font-medium text-[#202124] placeholder:text-gray-400 focus:outline-none transition-all shadow-[0_8px_30px_rgb(0,0,0,0.04)] hover:shadow-[0_8px_30px_rgb(0,0,0,0.08)] ${
error
? "border-red-200 focus:border-red-400"
: isAiMode
? "border-gray-100 focus:border-orange-400"
: "border-gray-100 focus:border-blue-400"
} ${!isAuthenticated ? "bg-gray-50 cursor-not-allowed" : ""}`}
/>

<div className="absolute left-5 top-1/2 -translate-y-1/2 text-gray-300">
{isAiMode ? <ChefHat size={20} /> : <Plus size={20} />}
</div>

<button
type="submit"
disabled={isPending || !isAuthenticated}
className={`absolute right-3 top-1/2 -translate-y-1/2 px-4 h-11 rounded-xl transition-all flex items-center gap-2 shadow-lg ${
!isAuthenticated
? "bg-gray-200 text-gray-400 shadow-none"
: isAiMode
? "bg-[#202124] text-white hover:bg-black"
: "bg-blue-600 text-white hover:bg-blue-700"
}`}
>
{isPending ? (
<Loader2 size={18} className="animate-spin" />
) : !isAuthenticated ? (
<Lock size={18} />
) : isAiMode ? (
<>
<span className="text-xs font-bold uppercase tracking-wider hidden sm:block">
Order
</span>
<UtensilsCrossed size={18} />
</>
) : (
<>
<span className="text-xs font-bold uppercase tracking-wider hidden sm:block">
Add
</span>
<Plus size={18} />
</>
)}
</button>
</form>

{error && (
<div className="mt-3 ml-4 flex items-center gap-2 text-red-500">
<div className="w-1 h-1 rounded-full bg-red-500 animate-pulse" />
<p className="text-[10px] font-bold uppercase tracking-[0.2em]">
{error.replace("_", " ")}
</p>
</div>
)}
</div>
);
}
Loading