diff --git a/.gitignore b/.gitignore
index ea850c7..7502944 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,13 @@ dist-ssr
*.njsproj
*.sln
*.sw?
+
+# Environments
+.env
+.envrc
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
diff --git a/package-lock.json b/package-lock.json
index a775d28..38cad56 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -59,6 +59,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -1633,6 +1634,7 @@
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -1674,6 +1676,7 @@
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -1782,6 +1785,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -2037,6 +2041,7 @@
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -3042,6 +3047,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -3102,6 +3108,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -3111,6 +3118,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -3400,6 +3408,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -3521,6 +3530,7 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/src/api/get-bucketlist.js b/src/api/get-bucketlist.js
new file mode 100644
index 0000000..35ac1a0
--- /dev/null
+++ b/src/api/get-bucketlist.js
@@ -0,0 +1,19 @@
+async function getBucketlist(bucketlistId) {
+ const url = `${import.meta.env.VITE_API_URL}/bucketlists/${bucketlistId}`;
+ const response = await fetch(url, { method: "GET" });
+
+ if (!response.ok) {
+ const fallbackError = `Error fetching bucketlist with id ${bucketlistId}`;
+
+ const data = await response.json().catch(() => {
+ throw new Error(fallbackError);
+ });
+
+ const errorMessage = data?.detail ?? fallbackError;
+ throw new Error(errorMessage);
+ }
+
+ return await response.json();
+}
+
+export default getBucketlist;
\ No newline at end of file
diff --git a/src/components/RegisterForm.jsx b/src/components/RegisterForm.jsx
new file mode 100644
index 0000000..a1576d9
--- /dev/null
+++ b/src/components/RegisterForm.jsx
@@ -0,0 +1,171 @@
+import { useState } from "react";
+//import postRegister from "../api/post-register.js";
+//import postLogin from "../api/post-login.js";
+import { useNavigate, useOutletContext } from "react-router-dom";
+
+function RegisterForm() {
+ const navigateTo = useNavigate();
+ const { setIsLoggedIn } = useOutletContext();
+
+ const [formData, setFormData] = useState({
+ username: "",
+ email: "",
+ password: "",
+ password2: "",
+ firstName: "",
+ lastName: "",
+ });
+
+ const [errors, setErrors] = useState({});
+ const [isLoading, setIsLoading] = useState(false);
+
+ const handleChange = (event) => {
+ const { id, value } = event.target;
+ setFormData((prevData) => ({
+ ...prevData,
+ [id]: value,
+ }));
+ // Clear error for this field when user starts typing
+ if (errors[id]) {
+ setErrors((prev) => ({ ...prev, [id]: "" }));
+ }
+};
+
+ const handleSubmit = async (event) => {
+ event.preventDefault();
+ setErrors({});
+
+ // Validation
+ const newErrors = {};
+ if (!formData.username) newErrors.username = "Username is required";
+ if (!formData.email) newErrors.email = "Email is required";
+ if (!formData.password) newErrors.password = "Password is required";
+ if (formData.password !== formData.password2) {
+ newErrors.password2 = "Passwords don't match";
+ }
+
+ if (Object.keys(newErrors).length > 0) {
+ setErrors(newErrors);
+ return;
+ }
+
+ setIsLoading(true);
+
+ try {
+ // Register the user COME BACK TO THIS AFTER post-register.js and post-login.js are ACTIONED
+ //await postRegister(
+ //formData.username,
+ //formData.email,
+ //formData.password,
+ //formData.firstName,
+ //formData.lastName
+ //);
+
+ // Automatically log them in
+ //const loginResponse = await postLogin(formData.username, formData.password);
+ //window.localStorage.setItem("token", loginResponse.token);
+ //window.localStorage.setItem("userId", loginResponse.user_id);
+ //setIsLoggedIn(true);
+
+ // Redirect to home
+ //navigateTo("/account");
+ console.log("Register from submitted", formData);
+ navigateTo("/");
+ } catch (error) {
+ setErrors({ general: error.message || "Registration failed. Please try again." });
+ } finally {
+ setIsLoading(false);
+ }
+ };
+
+ return (
+
+ );
+}
+
+export default RegisterForm;
\ No newline at end of file
diff --git a/src/main.jsx b/src/main.jsx
index ef4eb10..94c34c3 100644
--- a/src/main.jsx
+++ b/src/main.jsx
@@ -6,10 +6,14 @@ import Layout from "./layout";
import HomePage from "./pages/HomePage.jsx";
import LoginPage from "./pages/LoginPage.jsx";
import AccountPage from "./pages/AccountPage.jsx";
+import BucketListsPage from "./pages/BucketListsPage";
+import SingleListView from "./pages/SingleListView.jsx";
+import RegisterPage from "./pages/RegisterPage";
import InviteAcceptPage from "./pages/InviteAcceptPage.jsx";
import { AuthProvider } from "./components/AuthProvider.jsx"
import GoogleOAuthCallback from "./components/GoogleOAuthCallback.jsx";
+import NotFound from "./pages/NotFoundPage";
import "./main.css"
const router = createBrowserRouter([
@@ -23,10 +27,12 @@ const router = createBrowserRouter([
children: [
{ index: true, element: },
{ path: "/login", element: },
+ { path: "/register", element: },
{ path: "/dashboard", element: },
{ path: "/oauth/google/callback", element: },
- { path: "/invites/:token", element: },
- { path: "/bucketlists/:id", element: },
+ { path: "/bucketlists", element: },
+ { path: "/bucketlists/:id", element: },
+ { path: "*", element: },
],
},
]);
diff --git a/src/pages/AccountPage.jsx b/src/pages/AccountPage.jsx
deleted file mode 100644
index ee75e93..0000000
--- a/src/pages/AccountPage.jsx
+++ /dev/null
@@ -1,36 +0,0 @@
-import { useState, useEffect } from "react";
-import { useAuth } from "../hooks/use-auth";
-import { Navigate } from "react-router-dom";
-import useUser from "../hooks/use-user";
-import Dashboard from "../components/Dashboard/Dashboard";
-
-function AccountPage() {
- const { auth, setAuth } = useAuth();
- const { user, isLoading, error } = useUser();
- const [currentUser, setCurrentUser] = useState(null);
-
- useEffect(() => {
- if (user) setCurrentUser(user);
- }, [user]);
-
- if (!auth?.access) return ;
- if (isLoading || !currentUser)
- return (
-
- Loading your account...
-
- );
- if (error)
- return (
- {error.message}
- );
-
- const handleUserUpdate = (updatedUser) => {
- setCurrentUser(updatedUser);
- setAuth((prev) => ({ ...prev, user: updatedUser }));
- };
-
- return ;
-}
-
-export default AccountPage;
diff --git a/src/pages/BucketListsPage.jsx b/src/pages/BucketListsPage.jsx
new file mode 100644
index 0000000..9259d3e
--- /dev/null
+++ b/src/pages/BucketListsPage.jsx
@@ -0,0 +1,3 @@
+export default function BucketListsPage() {
+ return Bucket Lists
+}
\ No newline at end of file
diff --git a/src/pages/LoginPage.jsx b/src/pages/LoginPage.jsx
index 98c919e..35a069c 100644
--- a/src/pages/LoginPage.jsx
+++ b/src/pages/LoginPage.jsx
@@ -118,4 +118,4 @@ function LoginPage() {
);
}
-export default LoginPage;
\ No newline at end of file
+export default LoginPage;
diff --git a/src/pages/NotFoundPage.jsx b/src/pages/NotFoundPage.jsx
new file mode 100644
index 0000000..3fadffe
--- /dev/null
+++ b/src/pages/NotFoundPage.jsx
@@ -0,0 +1,10 @@
+import { Link } from "react-router-dom";
+
+export default function NotFound() {
+ return (
+
+
404 - Page Not Found
+ Back to Home
+
+ )
+}
\ No newline at end of file
diff --git a/src/pages/RegisterPage.jsx b/src/pages/RegisterPage.jsx
new file mode 100644
index 0000000..d6dd4a5
--- /dev/null
+++ b/src/pages/RegisterPage.jsx
@@ -0,0 +1,19 @@
+import { Link } from "react-router-dom";
+import RegisterForm from "../components/RegisterForm";
+
+function RegisterPage() {
+ return (
+
+
+
Create Account
+
Join Bucket List
+
+
+ Already have an account? Login here
+
+
+
+ );
+}
+
+export default RegisterPage;
\ No newline at end of file
diff --git a/src/pages/SingleListView.jsx b/src/pages/SingleListView.jsx
new file mode 100644
index 0000000..6c82836
--- /dev/null
+++ b/src/pages/SingleListView.jsx
@@ -0,0 +1,483 @@
+import { useState, useEffect } from "react";
+import { useParams } from "react-router-dom";
+
+// Kickit Colours
+const COLORS = {
+ primary: "#6B4EAA",
+ accent: "#FF5A5F",
+ border: "#A78BFA",
+ bodyText: "#0F172A",
+ mutedText: "#6B6880",
+ surface: "#EEEAF7",
+ ctaText: "#FFFFFF",
+ background: "#F7F6FB",
+ white: "#FFFFFF",
+};
+
+// Tag Colours
+const TAG_COLORS = {
+ travel: { bg: "#EDE9F7", text: "#6B4EAA", dot: "#6B4EAA" },
+ food: { bg: "#FFF0F0", text: "#FF5A5F", dot: "#FF5A5F" },
+ adventure: { bg: "#EDE9F7", text: "#7C3AED", dot: "#7C3AED" },
+ wellness: { bg: "#F0FDF4", text: "#16A34A", dot: "#16A34A" },
+ social: { bg: "#FFF0F0", text: "#FF5A5F", dot: "#FF5A5F" },
+ learning: { bg: "#EDE9F7", text: "#6B4EAA", dot: "#6B4EAA" },
+ creative: { bg: "#EDE9F7", text: "#A78BFA", dot: "#A78BFA" },
+ other: { bg: "#EEEAF7", text: "#6B6880", dot: "#6B6880" },
+};
+
+// Helpers
+function formatDate(iso) {
+ if (!iso) return null;
+ return new Date(iso).toLocaleDateString("en-AU", { day: "numeric", month: "short", year: "numeric" });
+}
+function formatTime(iso) {
+ if (!iso) return null;
+ return new Date(iso).toLocaleTimeString("en-AU", { hour: "2-digit", minute: "2-digit" });
+}
+function daysUntil(iso) {
+ return Math.ceil((new Date(iso) - new Date()) / (1000 * 60 * 60 * 24));
+}
+
+// Icons
+const TagIcon = () => (
+
+
+
+
+);
+const CheckIcon = () => (
+
+
+
+);
+const ClockIcon = () => (
+
+
+
+);
+const CalendarIcon = () => (
+
+
+
+);
+const GlobeIcon = () => (
+
+
+
+);
+const LockIcon = () => (
+
+
+
+);
+const TrashIcon = () => (
+
+
+
+
+);
+const UserIcon = () => (
+
+
+
+);
+
+// Delete Modal
+function DeleteConfirmModal({ item, onConfirm, onCancel }) {
+ return (
+
+
+
🗑️
+
+ Get rid of it?
+
+
+ "{item.title} " is gone.
+
+
+ Cancel
+ Delete
+
+
+
+ );
+}
+
+// Item Card
+function ItemCard({ item, onDelete }) {
+ const [hovering, setHovering] = useState(false);
+ const tag = TAG_COLORS[item.tag] || TAG_COLORS.other;
+
+ return (
+ setHovering(true)}
+ onMouseLeave={() => setHovering(false)}
+ style={{
+ background: item.is_completed ? COLORS.background : COLORS.white,
+ border: `1px solid ${hovering ? COLORS.border : "#E2DAF5"}`,
+ borderRadius: "16px",
+ padding: "20px 24px",
+ transition: "all 0.2s ease",
+ transform: hovering ? "translateY(-2px)" : "none",
+ boxShadow: hovering
+ ? "0 8px 32px rgba(107,78,170,0.12)"
+ : "0 1px 4px rgba(107,78,170,0.05)",
+ opacity: item.is_completed ? 0.75 : 1,
+ }}
+ >
+ {/* Top row */}
+
+
+ {/* Completion circle */}
+
+ {item.is_completed && }
+
+
+
+
+ {item.title}
+
+ {item.description && (
+
+ {item.description}
+
+ )}
+
+
+
+ {/* Delete button */}
+
onDelete(item)}
+ style={{
+ background: hovering ? "#FFF0F0" : "transparent",
+ border: `1px solid ${hovering ? "#FFCDD2" : "transparent"}`,
+ color: hovering ? COLORS.accent : "#C4B5D4",
+ borderRadius: "8px", padding: "6px 8px",
+ cursor: "pointer", flexShrink: 0,
+ transition: "all 0.2s ease", display: "flex", alignItems: "center"
+ }}
+ >
+
+
+
+
+ {/* Meta row */}
+
+ {/* Tag badge */}
+
+
+
+ {item.tag}
+
+
+
+ {formatDate(item.date_created)}
+
+
+ {formatTime(item.date_created)}
+
+
+ {item.created_by?.username}
+
+
+ {item.is_completed && item.completed_at && (
+
+ Completed {formatDate(item.completed_at)}
+
+ )}
+
+
+ );
+}
+
+// Main Page
+export default function SingleListView() {
+ const [data, setData] = useState(null);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+ const [deleteTarget, setDeleteTarget] = useState(null);
+ const [filter, setFilter] = useState("all");
+
+ const { id } = useParams();
+ const API_BASE = import.meta.env.VITE_API_URL;
+
+useEffect(() => {
+ async function fetchData() {
+ try {
+ const token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0b2tlbl90eXBlIjoiYWNjZXNzIiwiZXhwIjoxNzcyOTczMTE3LCJpYXQiOjE3NzI5Njk1MzIsImp0aSI6ImI0MjYxOTdiOTcxYjQyMTJiNDM3MGYyZDU3OGY1MjQ4IiwidXNlcl9pZCI6IjEifQ.-mJDUM68HuuN1Huab5kWN-oW21r8eRHhGreE-26Xe2o";
+
+ const [listRes, itemsRes] = await Promise.all([
+ fetch(`${API_BASE}/bucketlists/${id}/`, {
+ credentials: "include",
+ headers: {
+ "Authorization": `Bearer ${token}`
+ }
+ }),
+ fetch(`${API_BASE}/bucketlists/${id}/items/`, {
+ credentials: "include",
+ headers: {
+ "Authorization": `Bearer ${token}`
+ }
+ }),
+ ]);
+ if (!listRes.ok || !itemsRes.ok) throw new Error("Failed to fetch data");
+ const list = await listRes.json();
+ const items = await itemsRes.json();
+ setData({ ...list, items });
+ } catch (err) {
+ setError(err.message);
+ } finally {
+ setLoading(false);
+ }
+ }
+ fetchData();
+ }, [id]);
+
+ if (loading) return (
+
+ Loading...
+
+ );
+
+ if (error || !data) return (
+
+ {error || "Something went wrong."}
+
+ );
+
+ const completed = data.items.filter(i => i.is_completed).length;
+ const total = data.items.length;
+ const progress = total > 0 ? Math.round((completed / total) * 100) : 0;
+ const daysLeft = data.has_deadline && data.deadline ? daysUntil(data.deadline) : null;
+
+ const filteredItems = data.items.filter(item => {
+ if (filter === "complete") return item.is_completed;
+ if (filter === "pending") return !item.is_completed;
+ return true;
+ });
+
+ const handleDeleteConfirm = async () => {
+ try {
+ const res = await fetch(`${API_BASE}/items/${deleteTarget.id}/`, {
+ method: "DELETE", credentials: "include",
+ });
+ if (!res.ok) throw new Error("Delete failed");
+ setData(prev => ({ ...prev, items: prev.items.filter(i => i.id !== deleteTarget.id) }));
+ } catch (err) {
+ alert(err.message);
+ } finally {
+ setDeleteTarget(null);
+ }
+ };
+
+ return (
+
+
+
+
+
+ {/* Breadcrumb */}
+
+ My Lists
+ ›
+ Detail View
+
+
+ {/* Header */}
+
+
+
+
+ {data.title}
+
+
+ {data.description}
+
+
+
+ {/* Badges */}
+
+
+ {data.is_public ? : }
+ {data.is_public ? "Public" : "Private"}
+
+
+
+
+ {data.is_open ? "Open" : "Archived"}
+
+
+
+
+ {/* Meta row */}
+
+
+ Created {formatDate(data.date_created)}
+
+
+ {data.owner?.username}
+
+ {daysLeft !== null && (
+
+
+ {daysLeft > 0 ? `${daysLeft} days until deadline` : "Deadline passed"}
+
+ )}
+
+
+
+ {/* Progress card */}
+
+
+
+ Progress
+
+
+ {completed}/{total}
+
+
+
+
{progress}% complete
+
+
+ {/* Filter tabs */}
+
+ {["all", "pending", "complete"].map(f => (
+ setFilter(f)}
+ style={{
+ padding: "7px 20px", borderRadius: "20px",
+ border: `1px solid ${filter === f ? COLORS.primary : "#E2DAF5"}`,
+ background: filter === f ? COLORS.primary : COLORS.white,
+ color: filter === f ? COLORS.ctaText : COLORS.mutedText,
+ cursor: "pointer", fontSize: "13px", fontWeight: "600",
+ transition: "all 0.15s ease", textTransform: "capitalize",
+ fontFamily: "'Inter', sans-serif"
+ }}
+ >
+ {f}
+
+ ))}
+
+
+ {/* Items list */}
+
+ {filteredItems.length === 0 ? (
+
+ Nothing here yet — make a plan, do something new!
+
+ ) : filteredItems.map(item => (
+
+ ))}
+
+
+
+ {/* Delete modal */}
+ {deleteTarget && (
+
setDeleteTarget(null)}
+ />
+ )}
+
+ );
+}
\ No newline at end of file