From 07e8c04c49d3fa6bab4fa0352cb5330b9bea96fa Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 10:52:35 -0500 Subject: [PATCH 01/19] fix(api): sort training logs by date ascending --- server/mongodb/actions/findAllTrainingLogs.ts | 1 - src/app/api/training-log/route.ts | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/mongodb/actions/findAllTrainingLogs.ts b/server/mongodb/actions/findAllTrainingLogs.ts index a25a84c..fd38f6a 100644 --- a/server/mongodb/actions/findAllTrainingLogs.ts +++ b/server/mongodb/actions/findAllTrainingLogs.ts @@ -7,7 +7,6 @@ const findAllTrainingLogs = async (): Promise => { const logs = (await TrainingLog.find({}).exec()).sort((a, b) => a.date < b.date ? -1 : b.date > a.date ? 1 : 0 ); - console.log('Sorted training logs ', logs); return logs; } catch (err) { console.error(`[ERROR]: Error encountered while creating user: ${err}`); diff --git a/src/app/api/training-log/route.ts b/src/app/api/training-log/route.ts index eb189e5..4d4ef81 100644 --- a/src/app/api/training-log/route.ts +++ b/src/app/api/training-log/route.ts @@ -44,9 +44,11 @@ export const GET = async (req: NextRequest): Promise => { }); animalLogs?.forEach((a) => logs.push(a)); } + logs = logs.sort((a, b) => + a.date < b.date ? -1 : b.date > a.date ? 1 : 0 + ); } - console.log('Training logs ', logs); return new Response(JSON.stringify({ data: logs }), { status: 200 }); }; From 5523de5deb711550a7cd6aab40474510a64b86ac Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 11:03:11 -0500 Subject: [PATCH 02/19] fix: input data type for animal card --- src/app/dashboard/animals/page.tsx | 97 ++++++++++++++++-------------- src/components/AnimalCard.tsx | 25 +++----- 2 files changed, 59 insertions(+), 63 deletions(-) diff --git a/src/app/dashboard/animals/page.tsx b/src/app/dashboard/animals/page.tsx index 3cd285b..17edcc8 100644 --- a/src/app/dashboard/animals/page.tsx +++ b/src/app/dashboard/animals/page.tsx @@ -1,4 +1,4 @@ -"use client" +'use client'; import LoginForm from '@/components/LoginForm'; import TrainingLogCard, { mockData } from '@/components/TrainingLogCard'; import Link from 'next/link'; @@ -8,50 +8,53 @@ import { useEffect, useState } from 'react'; import { useSession } from 'next-auth/react'; type LogData = { - _id: string; - user: { _id: string; fullName: string }; - name: string; - animal_name: string; - breed: string; - hours: number; - url: string; + _id: string; + user: { _id: string; fullName: string }; + name: string; + animal_name: string; + breed: string; + hours: number; + url: string; }; const mapDbAnimalToLogData = (dbAnimal: any): LogData => ({ - _id: dbAnimal._id, - user: { - _id: dbAnimal.owner._id, - fullName: dbAnimal.owner.fullName - }, - name: dbAnimal.name, - animal_name: dbAnimal.name, - breed: dbAnimal.breed, - hours: dbAnimal.hoursTrained, - url: dbAnimal.profilePicture, + _id: dbAnimal._id, + user: { + _id: dbAnimal.owner._id, + fullName: dbAnimal.owner.fullName, + }, + name: dbAnimal.name, + animal_name: dbAnimal.name, + breed: dbAnimal.breed, + hours: dbAnimal.hoursTrained, + url: dbAnimal.profilePicture, }); export default function AnimalsPage() { const { data: session, status } = useSession(); const [animals, setAnimals] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { - const fetchTrainingLogs = async () => { - try { - const res = await fetch('/api/animal', { method: 'GET', credentials: 'include' }); - if (!res.ok) throw new Error("Failed to fetch logs"); - const data = await res.json(); - console.log("test") - console.log(data) - setAnimals(data?.data?.map(mapDbAnimalToLogData) ?? []); - } catch (err) { - console.error(err); - } finally { - setLoading(false); - } - }; + const fetchTrainingLogs = async () => { + try { + const res = await fetch('/api/animal', { + method: 'GET', + credentials: 'include', + }); + if (!res.ok) throw new Error('Failed to fetch animals'); + const data = await res.json(); + console.log('test'); + console.log(data); + setAnimals(data?.data?.map(mapDbAnimalToLogData) ?? []); + } catch (err) { + console.error(err); + } finally { + setLoading(false); + } + }; - if (session) { - fetchTrainingLogs(); - } + if (session) { + fetchTrainingLogs(); + } }, [session]); return ( @@ -73,15 +76,19 @@ export default function AnimalsPage() { - - -
-
- {loading &&
Loading...
} - {!loading && animals.length === 0 &&
No animals found
} - {!loading && - animals.map((animal) => )} -
- + + +
+
+ {loading &&
Loading...
} + {!loading && animals.length === 0 && ( +
No animals found
+ )} + {!loading && + animals.map((animal) => ( + + ))} +
+ ); } diff --git a/src/components/AnimalCard.tsx b/src/components/AnimalCard.tsx index 7cd8f8c..a13d91d 100644 --- a/src/components/AnimalCard.tsx +++ b/src/components/AnimalCard.tsx @@ -1,23 +1,10 @@ -"use client"; -import mongoose, { ObjectId } from "mongoose"; -import { useEffect } from "react"; -import Image from "next/image"; -import Link from "next/link"; +'use client'; +import { AnimalDocument } from '../../server/mongodb/models/Animal'; - -type LogData = { - _id: ObjectId | string; - user: { _id: ObjectId | string; fullName: string } - name: string; - animal_name: string; - breed: string; - hours: number; - url: string; -}; -const AnimalCard = ({ data }: { data: LogData }) => { +const AnimalCard = ({ data }: { data: AnimalDocument }) => { return (
- +
{data.name.charAt(0)} @@ -29,7 +16,9 @@ const AnimalCard = ({ data }: { data: LogData }) => {
{data.user.fullName + - " - Trained " + data.hours + " hours"} + ' - Trained ' + + data.hours + + ' hours'}
From 46d9ab996b9ebfc5ea1c0960a4e5f8f5f70b6567 Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 11:31:32 -0500 Subject: [PATCH 03/19] feat(ui): implement reusable animals grid component --- src/app/dashboard/admin/animals/page.tsx | 19 +---- src/app/dashboard/admin/layout.tsx | 2 +- .../dashboard/admin/training-logs/page.tsx | 25 +------ src/app/dashboard/admin/users/page.tsx | 4 +- src/app/dashboard/animals/page.tsx | 73 +++++-------------- src/app/dashboard/training-logs/page.tsx | 28 +------ src/components/AnimalCard.tsx | 33 ++++++++- src/components/AnimalsGrid.tsx | 34 +++++++++ src/components/SideBar.tsx | 58 ++++++++------- src/components/TrainingLogCard.tsx | 2 +- src/components/TrainingLogList.tsx | 38 ++++++++++ 11 files changed, 166 insertions(+), 150 deletions(-) create mode 100644 src/components/AnimalsGrid.tsx create mode 100644 src/components/TrainingLogList.tsx diff --git a/src/app/dashboard/admin/animals/page.tsx b/src/app/dashboard/admin/animals/page.tsx index 46712ca..7b9a43e 100644 --- a/src/app/dashboard/admin/animals/page.tsx +++ b/src/app/dashboard/admin/animals/page.tsx @@ -3,7 +3,8 @@ import Link from 'next/link'; import Image from 'next/image'; import { AnimalDocument } from '../../../../../server/mongodb/models/Animal'; import { useEffect, useState } from 'react'; -import AnimalCard from '@/components/AnimalCard'; + +import AnimalsGrid from '@/components/AnimalsGrid'; export default function AdminAnimalsPage() { const [isLoading, setIsLoading] = useState(true); @@ -31,7 +32,7 @@ export default function AdminAnimalsPage() { fetchAnimals(); }, [isLoading]); return ( -
+

All Animals @@ -52,19 +53,7 @@ export default function AdminAnimalsPage() {


-
- {animals.map((a, i) => { - return ( -
- -
- ); - })} - {!isLoading && animals.length == 0 &&

No animals yet!

} -
+
); } diff --git a/src/app/dashboard/admin/layout.tsx b/src/app/dashboard/admin/layout.tsx index 270f6ae..809a312 100644 --- a/src/app/dashboard/admin/layout.tsx +++ b/src/app/dashboard/admin/layout.tsx @@ -13,7 +13,7 @@ export default function RootLayout({ const isAdmin = session?.user && (session?.user as any).admin; return isAdmin ? ( -
{children}
+ <>{children} ) : (

diff --git a/src/app/dashboard/admin/training-logs/page.tsx b/src/app/dashboard/admin/training-logs/page.tsx index eed7a43..8eb03af 100644 --- a/src/app/dashboard/admin/training-logs/page.tsx +++ b/src/app/dashboard/admin/training-logs/page.tsx @@ -1,8 +1,7 @@ 'use client'; -import TrainingLogCard, { mockData } from '@/components/TrainingLogCard'; import { useEffect, useState } from 'react'; import { TrainingLogDocument } from '../../../../../server/mongodb/models/TrainingLog'; -import Skeleton from 'react-loading-skeleton'; +import TrainingLogList from '@/components/TrainingLogList'; export default function AdminTrainingLogsPage() { const [isLoading, setIsLoading] = useState(true); @@ -31,7 +30,7 @@ export default function AdminTrainingLogsPage() { }, [isLoading]); return ( -

+

All training Logs @@ -39,25 +38,7 @@ export default function AdminTrainingLogsPage() {


-
- {isLoading ? ( - - ) : ( - logs.map((l, i) => ( - - )) - )} - {!isLoading && logs.length == 0 &&

No training logs yet!

} -
+
); } diff --git a/src/app/dashboard/admin/users/page.tsx b/src/app/dashboard/admin/users/page.tsx index bed69c6..84231ce 100644 --- a/src/app/dashboard/admin/users/page.tsx +++ b/src/app/dashboard/admin/users/page.tsx @@ -32,7 +32,7 @@ export default function AdminUsersPage() { }, [isLoading]); return ( -
+

All Users @@ -40,7 +40,7 @@ export default function AdminUsersPage() {


-
+
{isLoading ? ( ) : ( diff --git a/src/app/dashboard/animals/page.tsx b/src/app/dashboard/animals/page.tsx index 17edcc8..ed7e2dc 100644 --- a/src/app/dashboard/animals/page.tsx +++ b/src/app/dashboard/animals/page.tsx @@ -1,61 +1,36 @@ 'use client'; -import LoginForm from '@/components/LoginForm'; -import TrainingLogCard, { mockData } from '@/components/TrainingLogCard'; import Link from 'next/link'; import Image from 'next/image'; import AnimalCard from '@/components/AnimalCard'; import { useEffect, useState } from 'react'; -import { useSession } from 'next-auth/react'; +import { AnimalDocument } from '../../../../server/mongodb/models/Animal'; +import AnimalsGrid from '@/components/AnimalsGrid'; -type LogData = { - _id: string; - user: { _id: string; fullName: string }; - name: string; - animal_name: string; - breed: string; - hours: number; - url: string; -}; - -const mapDbAnimalToLogData = (dbAnimal: any): LogData => ({ - _id: dbAnimal._id, - user: { - _id: dbAnimal.owner._id, - fullName: dbAnimal.owner.fullName, - }, - name: dbAnimal.name, - animal_name: dbAnimal.name, - breed: dbAnimal.breed, - hours: dbAnimal.hoursTrained, - url: dbAnimal.profilePicture, -}); export default function AnimalsPage() { - const { data: session, status } = useSession(); - const [animals, setAnimals] = useState([]); - const [loading, setLoading] = useState(true); + const [isLoading, setIsLoading] = useState(true); + const [animals, setAnimals] = useState([]); + useEffect(() => { - const fetchTrainingLogs = async () => { + const fetchAnimals = async () => { try { - const res = await fetch('/api/animal', { + const response = await fetch('/api/animal', { method: 'GET', credentials: 'include', }); - if (!res.ok) throw new Error('Failed to fetch animals'); - const data = await res.json(); - console.log('test'); - console.log(data); - setAnimals(data?.data?.map(mapDbAnimalToLogData) ?? []); - } catch (err) { - console.error(err); - } finally { - setLoading(false); + const respBody = await response.json(); + const animals: AnimalDocument[] = respBody?.data; + + if (animals != undefined) { + setAnimals(animals); + } + setIsLoading(false); + } catch (error) { + setIsLoading(false); } }; - if (session) { - fetchTrainingLogs(); - } - }, [session]); + fetchAnimals(); + }, [isLoading]); return (
@@ -77,18 +52,8 @@ export default function AnimalsPage() {
-
-
- {loading &&
Loading...
} - {!loading && animals.length === 0 && ( -
No animals found
- )} - {!loading && - animals.map((animal) => ( - - ))} -
+
); } diff --git a/src/app/dashboard/training-logs/page.tsx b/src/app/dashboard/training-logs/page.tsx index 9feaf84..e104a8c 100644 --- a/src/app/dashboard/training-logs/page.tsx +++ b/src/app/dashboard/training-logs/page.tsx @@ -1,11 +1,10 @@ 'use client'; -import TrainingLogCard, { mockData } from '@/components/TrainingLogCard'; import Link from 'next/link'; import Image from 'next/image'; import { useEffect, useState } from 'react'; import { useSession } from 'next-auth/react'; import { TrainingLogDocument } from '../../../../server/mongodb/models/TrainingLog'; -import Skeleton from 'react-loading-skeleton'; +import TrainingLogList from '@/components/TrainingLogList'; export default function TrainingLogsPage() { const { data: session } = useSession(); @@ -35,9 +34,8 @@ export default function TrainingLogsPage() { fetchTrainingLogs(); }, [isLoading]); - console.log('Training logs ', logs); return ( -
+

Training logs @@ -59,27 +57,7 @@ export default function TrainingLogsPage() {
-
- {isLoading ? ( - - ) : ( - logs.map((l, i) => ( - - )) - )} - {!isLoading && logs.length == 0 && ( -

No training logs yet!

- )} -
+

); } diff --git a/src/components/AnimalCard.tsx b/src/components/AnimalCard.tsx index a13d91d..6d497c8 100644 --- a/src/components/AnimalCard.tsx +++ b/src/components/AnimalCard.tsx @@ -1,7 +1,31 @@ 'use client'; +import { useEffect, useState } from 'react'; import { AnimalDocument } from '../../server/mongodb/models/Animal'; +import { UserDocument } from '../../server/mongodb/models/User'; const AnimalCard = ({ data }: { data: AnimalDocument }) => { + const [user, setUser] = useState | null>(null); + + useEffect(() => { + const fetchUser = async () => { + try { + const response = await fetch(`/api/user/${data.owner}`); + const respBody = await response.json(); + const user = respBody.data; + if (user != undefined) { + setUser(user); + } else { + setUser({ fullName: 'Long Lam' }); + } + } catch (error) { + setUser({ fullName: 'Long Lam' }); + } + }; + + if (!user) { + fetchUser(); + } + }, [user]); return (
@@ -15,10 +39,11 @@ const AnimalCard = ({ data }: { data: AnimalDocument }) => { {data.name + ' - ' + data.breed}
- {data.user.fullName + - ' - Trained ' + - data.hours + - ' hours'} + {user && + user.fullName + + ' - Trained ' + + data.hoursTrained + + ' hours'}
diff --git a/src/components/AnimalsGrid.tsx b/src/components/AnimalsGrid.tsx new file mode 100644 index 0000000..fe31f45 --- /dev/null +++ b/src/components/AnimalsGrid.tsx @@ -0,0 +1,34 @@ +import { AnimalDocument } from '../../server/mongodb/models/Animal'; +import AnimalCard from './AnimalCard'; + +const AnimalsGrid = ({ + isLoading, + animals, +}: { + isLoading: boolean; + animals: AnimalDocument[]; +}) => { + return ( + <> + {!isLoading && animals.length == 0 && ( +
+

No animals yet!

+
+ )} +
+ {animals.map((a, i) => { + return ( +
+ +
+ ); + })} +
+ + ); +}; + +export default AnimalsGrid; diff --git a/src/components/SideBar.tsx b/src/components/SideBar.tsx index 32826b6..8a17547 100644 --- a/src/components/SideBar.tsx +++ b/src/components/SideBar.tsx @@ -101,32 +101,36 @@ const SideBar = () => { url="/dashboard/animals" />
-

Admin Access

- - - - - - -
+ {(session?.user as any)?.admin && ( + <> +

Admin Access

+ + + + + + +
+ + )} -
+
{session?.user?.name?.charAt(0)} @@ -143,10 +147,12 @@ const SideBar = () => { )}
- +
+ +
); }; -export default SideBar; \ No newline at end of file +export default SideBar; diff --git a/src/components/TrainingLogCard.tsx b/src/components/TrainingLogCard.tsx index ab05c68..46b76d0 100644 --- a/src/components/TrainingLogCard.tsx +++ b/src/components/TrainingLogCard.tsx @@ -82,7 +82,7 @@ const TrainingLogCard = ({ data }: { data: LogData }) => { const dateObj = new Date(dateStr); return ( -
+
{dateObj.getDate()} diff --git a/src/components/TrainingLogList.tsx b/src/components/TrainingLogList.tsx new file mode 100644 index 0000000..b9b6107 --- /dev/null +++ b/src/components/TrainingLogList.tsx @@ -0,0 +1,38 @@ +import Skeleton from 'react-loading-skeleton'; +import { TrainingLogDocument } from '../../server/mongodb/models/TrainingLog'; +import TrainingLogCard from './TrainingLogCard'; + +const TrainingLogList = ({ + isLoading, + logs, +}: { + isLoading: boolean; + logs: TrainingLogDocument[]; +}) => { + console.log('Training logs ', logs); + return ( +
+ {isLoading ? ( + + ) : ( + logs.map((l, i) => ( + + )) + )} + {!isLoading && logs.length == 0 && ( +

No training logs yet!

+ )} +
+ ); +}; + +export default TrainingLogList; From 0516267a480fd93964c743aa17b8675135988b56 Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 11:46:00 -0500 Subject: [PATCH 04/19] feat(api): fix hardcoded profile pic --- src/app/api/animal/route.ts | 1 - src/app/dashboard/admin/animals/page.tsx | 2 +- src/app/dashboard/admin/training-logs/page.tsx | 2 +- src/app/dashboard/animals/new/page.tsx | 2 +- src/app/dashboard/animals/page.tsx | 2 +- src/app/dashboard/layout.tsx | 4 ++-- src/app/dashboard/training-logs/new/page.tsx | 2 +- src/app/dashboard/training-logs/page.tsx | 2 +- src/app/layout.tsx | 2 +- 9 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/app/api/animal/route.ts b/src/app/api/animal/route.ts index a830694..5acfdc7 100644 --- a/src/app/api/animal/route.ts +++ b/src/app/api/animal/route.ts @@ -45,7 +45,6 @@ export const POST = async (req: NextRequest): Promise => { const userId = (session?.user as any)._id; data.owner = userId; - data.profilePicture = '/images/appLogo.png'; const animals = await findAnimalsByOwner({ id: userId }); const animalsMatchingName = animals?.filter((a) => a.name == data.name); diff --git a/src/app/dashboard/admin/animals/page.tsx b/src/app/dashboard/admin/animals/page.tsx index 7b9a43e..395f037 100644 --- a/src/app/dashboard/admin/animals/page.tsx +++ b/src/app/dashboard/admin/animals/page.tsx @@ -32,7 +32,7 @@ export default function AdminAnimalsPage() { fetchAnimals(); }, [isLoading]); return ( -
+

All Animals diff --git a/src/app/dashboard/admin/training-logs/page.tsx b/src/app/dashboard/admin/training-logs/page.tsx index 8eb03af..dfd1626 100644 --- a/src/app/dashboard/admin/training-logs/page.tsx +++ b/src/app/dashboard/admin/training-logs/page.tsx @@ -30,7 +30,7 @@ export default function AdminTrainingLogsPage() { }, [isLoading]); return ( -
+

All training Logs diff --git a/src/app/dashboard/animals/new/page.tsx b/src/app/dashboard/animals/new/page.tsx index c7e8b76..51e26ed 100644 --- a/src/app/dashboard/animals/new/page.tsx +++ b/src/app/dashboard/animals/new/page.tsx @@ -2,7 +2,7 @@ import EditAnimalForm from '@/components/EditAnimalForm'; export default function SignInPage() { return ( -
+

New Animal

diff --git a/src/app/dashboard/animals/page.tsx b/src/app/dashboard/animals/page.tsx index ed7e2dc..9190d09 100644 --- a/src/app/dashboard/animals/page.tsx +++ b/src/app/dashboard/animals/page.tsx @@ -33,7 +33,7 @@ export default function AnimalsPage() { }, [isLoading]); return ( -
+

Animals diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index 598a215..ccd866a 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -14,8 +14,8 @@ export default function RootLayout({ console.log('Active session data ', session); return ( -
-
+
+
diff --git a/src/app/dashboard/training-logs/new/page.tsx b/src/app/dashboard/training-logs/new/page.tsx index cb6902f..953bf84 100644 --- a/src/app/dashboard/training-logs/new/page.tsx +++ b/src/app/dashboard/training-logs/new/page.tsx @@ -2,7 +2,7 @@ import EditTrainingLogForm from '@/components/EditTrainingLogForm'; export default function SignInPage() { return ( -
+

New Training Log

diff --git a/src/app/dashboard/training-logs/page.tsx b/src/app/dashboard/training-logs/page.tsx index e104a8c..1aef23c 100644 --- a/src/app/dashboard/training-logs/page.tsx +++ b/src/app/dashboard/training-logs/page.tsx @@ -35,7 +35,7 @@ export default function TrainingLogsPage() { }, [isLoading]); return ( -
+

Training logs diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 64b6164..2d2d228 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -30,7 +30,7 @@ export default function RootLayout({ className={`${geistSans.variable} ${geistMono.variable} antialiased`} > -
+
From ff2c8db4a169e874ec806587995629b78f006e49 Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 12:43:08 -0500 Subject: [PATCH 05/19] feat(ui): make forms overflow on small layouts --- src/app/dashboard/admin/animals/page.tsx | 13 ------- .../dashboard/admin/training-logs/page.tsx | 2 +- src/app/dashboard/admin/users/page.tsx | 2 +- src/app/dashboard/animals/new/page.tsx | 6 +-- .../training-logs/edit/[id]/page.tsx | 4 +- src/app/dashboard/training-logs/new/page.tsx | 4 +- src/components/AnimalCard.tsx | 4 +- src/components/EditAnimalForm.tsx | 15 +++---- src/components/EditTrainingLogForm.tsx | 2 +- src/components/LoginForm.tsx | 39 ++++++++++++++----- src/components/NavBar.tsx | 2 +- 11 files changed, 52 insertions(+), 41 deletions(-) diff --git a/src/app/dashboard/admin/animals/page.tsx b/src/app/dashboard/admin/animals/page.tsx index 395f037..3870beb 100644 --- a/src/app/dashboard/admin/animals/page.tsx +++ b/src/app/dashboard/admin/animals/page.tsx @@ -37,19 +37,6 @@ export default function AdminAnimalsPage() {

All Animals

- -
- create new animal{' '} - - Create New - -
-

diff --git a/src/app/dashboard/admin/training-logs/page.tsx b/src/app/dashboard/admin/training-logs/page.tsx index dfd1626..19439ea 100644 --- a/src/app/dashboard/admin/training-logs/page.tsx +++ b/src/app/dashboard/admin/training-logs/page.tsx @@ -33,7 +33,7 @@ export default function AdminTrainingLogsPage() {

- All training Logs + All Training Logs

diff --git a/src/app/dashboard/admin/users/page.tsx b/src/app/dashboard/admin/users/page.tsx index 84231ce..01005fa 100644 --- a/src/app/dashboard/admin/users/page.tsx +++ b/src/app/dashboard/admin/users/page.tsx @@ -32,7 +32,7 @@ export default function AdminUsersPage() { }, [isLoading]); return ( -
+

All Users diff --git a/src/app/dashboard/animals/new/page.tsx b/src/app/dashboard/animals/new/page.tsx index 51e26ed..589e9dd 100644 --- a/src/app/dashboard/animals/new/page.tsx +++ b/src/app/dashboard/animals/new/page.tsx @@ -2,12 +2,12 @@ import EditAnimalForm from '@/components/EditAnimalForm'; export default function SignInPage() { return ( -
-

+
+

New Animal


-
+
{' '}
diff --git a/src/app/dashboard/training-logs/edit/[id]/page.tsx b/src/app/dashboard/training-logs/edit/[id]/page.tsx index d6c6206..9e6586a 100644 --- a/src/app/dashboard/training-logs/edit/[id]/page.tsx +++ b/src/app/dashboard/training-logs/edit/[id]/page.tsx @@ -2,12 +2,12 @@ import EditTrainingLogForm from '@/components/EditTrainingLogForm'; export default function SignInPage() { return ( -
+

Training logs


-
+
{' '}
diff --git a/src/app/dashboard/training-logs/new/page.tsx b/src/app/dashboard/training-logs/new/page.tsx index 953bf84..451bf14 100644 --- a/src/app/dashboard/training-logs/new/page.tsx +++ b/src/app/dashboard/training-logs/new/page.tsx @@ -2,12 +2,12 @@ import EditTrainingLogForm from '@/components/EditTrainingLogForm'; export default function SignInPage() { return ( -
+

New Training Log


-
+
{' '}
diff --git a/src/components/AnimalCard.tsx b/src/components/AnimalCard.tsx index 6d497c8..9cf4417 100644 --- a/src/components/AnimalCard.tsx +++ b/src/components/AnimalCard.tsx @@ -9,7 +9,9 @@ const AnimalCard = ({ data }: { data: AnimalDocument }) => { useEffect(() => { const fetchUser = async () => { try { - const response = await fetch(`/api/user/${data.owner}`); + const response = await fetch( + `/api/user/${data.owner._id.toString()}` + ); const respBody = await response.json(); const user = respBody.data; if (user != undefined) { diff --git a/src/components/EditAnimalForm.tsx b/src/components/EditAnimalForm.tsx index 33f2d1b..fb2c073 100644 --- a/src/components/EditAnimalForm.tsx +++ b/src/components/EditAnimalForm.tsx @@ -28,7 +28,8 @@ const EditAnimalForm = () => { } = useForm({ defaultValues: { hoursTrained: 1, - profilePicture: 'https://www.vidavetcare.com/wp-content/uploads/sites/234/2022/04/golden-retriever-dog-breed-info.jpeg', + profilePicture: + 'https://www.vidavetcare.com/wp-content/uploads/sites/234/2022/04/golden-retriever-dog-breed-info.jpeg', }, }); @@ -54,7 +55,7 @@ const EditAnimalForm = () => { toast('Animal created successfully!'); reset(); } catch (err) { - console.log + console.log; setError('root.serverError', { message: 'Failed to create a new animal', }); @@ -62,14 +63,14 @@ const EditAnimalForm = () => { } }; return ( -
+
{errors?.root?.serverError && (

{errors?.root?.serverError.message}

)}
@@ -81,9 +82,9 @@ const EditAnimalForm = () => { className="flex-1 p-4 text-2xl w-full rounded-lg " {...register('name', { required: true })} /> - {errors.name && ( + {errors.name?.type === 'required' && (

- {errors.name.message} + Animal name is required

)}
@@ -162,4 +163,4 @@ const EditAnimalForm = () => { ); }; -export default EditAnimalForm; \ No newline at end of file +export default EditAnimalForm; diff --git a/src/components/EditTrainingLogForm.tsx b/src/components/EditTrainingLogForm.tsx index 0cf5d9b..41a513a 100644 --- a/src/components/EditTrainingLogForm.tsx +++ b/src/components/EditTrainingLogForm.tsx @@ -156,7 +156,7 @@ const EditTrainingLogForm = () => {
diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index 795463a..1ba7822 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -23,17 +23,36 @@ const LoginForm = () => { const result = await signIn('credentials', { email: data.email, password: data.password, - callbackUrl: '/dashboard/training-logs', + + redirect: false, }); - console.log(result); - if (!result || result.error) { - if (result?.status == 401) { - setError('root.serverError', { - type: result.status as any, - message: 'Your email or password is incorrect.', - }); + console.log('Result ', result); + // callbackUrl: '/dashboard/training-logs', + + if (result) { + if (result?.ok) { + router.push('/dashboard/training-logs'); + } else { + if (result?.status == 401) { + setError('root.serverError', { + type: 'auth', + message: 'Your email or password is incorrect.', + }); + } else { + setError('root.serverError', { + type: 'general', + message: + 'Internal server error. Contact admin for more details', + }); + } } + } else { + setError('root.serverError', { + type: 'general', + message: + 'Internal server error. Contact admin for more details', + }); } }; @@ -46,7 +65,9 @@ const LoginForm = () => {

Log In

{errors?.root?.serverError && ( -

{errors?.root?.serverError?.message}

+

+ {errors?.root?.serverError?.message} +

)}
{ const { data: session } = useSession(); return ( -
+
Date: Thu, 20 Nov 2025 12:43:38 -0500 Subject: [PATCH 06/19] fix(crud): remove confusing log for find user by email --- server/mongodb/actions/findUserByEmail.ts | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/server/mongodb/actions/findUserByEmail.ts b/server/mongodb/actions/findUserByEmail.ts index 1405e64..78a919a 100644 --- a/server/mongodb/actions/findUserByEmail.ts +++ b/server/mongodb/actions/findUserByEmail.ts @@ -1,18 +1,20 @@ import connectDB from '..'; import { User, UserDocument } from '../models/User'; -const findUserByEmail = async ( - {email} : {email: string} -): Promise => { +const findUserByEmail = async ({ + email, +}: { + email: string; +}): Promise => { try { await connectDB(); const user = (await User.find({ email }).exec()).at(0); if (!user) { - throw new Error('User could not be found with the given email'); + return null; } return user; } catch (err) { - console.error(`[ERROR]: Error encountered while creating user: ${err}`); + console.error(err); return null; } }; From 2e321bcfa9d3b717b64c1ae7251c319d93d938d0 Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 13:12:59 -0500 Subject: [PATCH 07/19] feat(ui): make form overflow on smaller viewports --- src/app/dashboard/training-logs/new/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/dashboard/training-logs/new/page.tsx b/src/app/dashboard/training-logs/new/page.tsx index 451bf14..cd13500 100644 --- a/src/app/dashboard/training-logs/new/page.tsx +++ b/src/app/dashboard/training-logs/new/page.tsx @@ -2,7 +2,7 @@ import EditTrainingLogForm from '@/components/EditTrainingLogForm'; export default function SignInPage() { return ( -
+

New Training Log

From d1ba09e9178907fae68c2142e1bffeeacf2c855b Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 13:15:39 -0500 Subject: [PATCH 08/19] feat(ci): add token to env secrets --- .github/workflows/playwright.yml | 55 ++++++++++++++++---------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index e253887..032d1e8 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -1,31 +1,32 @@ name: Playwright Tests env: - NEXTAUTH_URL: http://localhost:3000 - DB_NAME: 'animal-tracking' - MONGO_DB: ${{ secrets.MONGO_DB }} + NEXTAUTH_URL: http://localhost:3000 + DB_NAME: 'animal-tracking' + MONGO_DB: ${{ secrets.MONGO_DB }} + AUTH_SECRET: ${{ secrets.AUTH_SECRET }} on: - push: - branches: [ main, master ] - pull_request: - branches: [ main, master ] + push: + branches: [main, master] + pull_request: + branches: [main, master] jobs: - test: - timeout-minutes: 60 - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - name: Install dependencies - run: npm ci - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - name: Run Playwright tests - run: npx playwright test - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 + test: + timeout-minutes: 60 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 From 951c9811a832fa72466c466aaac1873dac4c885e Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 13:15:58 -0500 Subject: [PATCH 09/19] chore: add dotenv to deps --- package-lock.json | 13 +++++++++++++ package.json | 1 + 2 files changed, 14 insertions(+) diff --git a/package-lock.json b/package-lock.json index 86424c9..b2cfb97 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "argon2": "^0.44.0", "axios": "^1.13.2", + "dotenv": "^17.2.3", "jose": "^6.1.2", "mongoose": "^8.19.4", "next": "16.0.1", @@ -1819,6 +1820,18 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", diff --git a/package.json b/package.json index 19f0aab..c79809b 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "argon2": "^0.44.0", "axios": "^1.13.2", + "dotenv": "^17.2.3", "jose": "^6.1.2", "mongoose": "^8.19.4", "next": "16.0.1", From 1930e409d4c72d32b809344b13b0f3785068c8a0 Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 13:18:45 -0500 Subject: [PATCH 10/19] ci: add e2e tests to verify functionality --- e2e/animals.spec.ts | 32 +++++++++++ e2e/auth.spec.ts | 53 +++++++++++++++++ e2e/example.spec.ts | 8 --- e2e/global-setup.ts | 35 +++++++++++ e2e/train-logs.spec.ts | 99 +++++++++++++++++++++++++++++++ playwright.config.ts | 128 +++++++++++++++++++++-------------------- 6 files changed, 284 insertions(+), 71 deletions(-) create mode 100644 e2e/animals.spec.ts create mode 100644 e2e/auth.spec.ts delete mode 100644 e2e/example.spec.ts create mode 100644 e2e/global-setup.ts create mode 100644 e2e/train-logs.spec.ts diff --git a/e2e/animals.spec.ts b/e2e/animals.spec.ts new file mode 100644 index 0000000..ef4cf8a --- /dev/null +++ b/e2e/animals.spec.ts @@ -0,0 +1,32 @@ +import test, { expect } from '@playwright/test'; + +test('create a new animal', async ({ page }) => { + await page.goto('/'); + // Expect a title "to contain" a substring. + await page.getByRole('link', { name: 'app logo Progress' }).click(); + await page.getByRole('link', { name: 'Log In' }).click(); + await page.getByRole('textbox', { name: 'Email' }).click(); + await page + .getByRole('textbox', { name: 'Email' }) + .fill('alice3@gatech.edu'); + await page.getByRole('textbox', { name: 'Email' }).press('Tab'); + await page.getByRole('textbox', { name: 'Password' }).fill('12345678'); + await page.getByRole('button', { name: 'Log In' }).click(); + + await page.getByRole('link', { name: 'Animals icon Animals' }).click(); + await page + .getByRole('link', { name: 'create new animal Create New' }) + .click(); + await page.getByRole('textbox', { name: 'Animal Name' }).click(); + await page.getByRole('textbox', { name: 'Animal Name' }).fill('Rover'); + await page.getByLabel('Breed').selectOption('German Shepherd'); + await page + .getByRole('spinbutton', { name: 'Total hours trained' }) + .fill('5'); + + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByText('Animal created successfully!')).toBeVisible(); + + await page.getByRole('link', { name: 'Animals icon Animals' }).click(); + await expect(page.getByText('Rover - German Shepherd')).toBeVisible(); +}); diff --git a/e2e/auth.spec.ts b/e2e/auth.spec.ts new file mode 100644 index 0000000..82afb16 --- /dev/null +++ b/e2e/auth.spec.ts @@ -0,0 +1,53 @@ +import { test, expect } from '@playwright/test'; + +test.beforeAll(async ({ request }) => { + const response = await request.post('/api/user/register', { + data: { + fullName: 'Admin User', + email: 'admin@gatech.edu', + password: '12345678', + confirmPassword: '12345678', + admin: true, + }, + }); + await expect(response).toBeOK(); +}); + +test('create admin user', async ({ page }) => { + await page.goto('/signup'); + // Expect a title "to contain" a substring. + await page.getByRole('textbox', { name: 'Full Name' }).click(); + await page.getByRole('textbox', { name: 'Full Name' }).fill('Admin User 2'); + await page.getByRole('textbox', { name: 'Full Name' }).press('Tab'); + await page + .getByRole('textbox', { name: 'Email' }) + .fill('admin2@gatech.edu'); + await page.getByRole('textbox', { name: 'Password', exact: true }).click(); + await page + .getByRole('textbox', { name: 'Password', exact: true }) + .fill('12345678'); + await page.getByRole('textbox', { name: 'Confirm Password' }).click(); + await page + .getByRole('textbox', { name: 'Confirm Password' }) + .fill('12345678'); + await page.getByRole('checkbox', { name: 'Admin Access' }).check(); + await page.getByRole('button', { name: 'Sign up' }).click(); + await expect( + page.getByRole('heading', { name: 'Create Account' }) + ).toBeVisible(); +}); + +test('login as admin user', async ({ page }) => { + await page.goto('/'); + // Expect a title "to contain" a substring. + await page.getByRole('link', { name: 'app logo Progress' }).click(); + await page.getByRole('link', { name: 'Log In' }).click(); + await page.getByRole('textbox', { name: 'Email' }).click(); + await page.getByRole('textbox', { name: 'Email' }).fill('admin@gatech.edu'); + await page.getByRole('textbox', { name: 'Email' }).press('Tab'); + await page.getByRole('textbox', { name: 'Password' }).fill('12345678'); + await page.getByRole('button', { name: 'Log In' }).click(); + await expect( + page.getByRole('heading', { name: 'Admin Access' }) + ).toBeVisible(); +}); diff --git a/e2e/example.spec.ts b/e2e/example.spec.ts deleted file mode 100644 index 8131631..0000000 --- a/e2e/example.spec.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { test, expect } from '@playwright/test'; - -test('has title', async ({ page }) => { - await page.goto('/signup') - // Expect a title "to contain" a substring. - await expect(page.getByRole('heading', { name: 'Create Account' })).toBeVisible(); -}); - diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts new file mode 100644 index 0000000..386481e --- /dev/null +++ b/e2e/global-setup.ts @@ -0,0 +1,35 @@ +import { chromium, expect, request, type FullConfig } from '@playwright/test'; +import connectDB from '../server/mongodb'; +import { User } from '../server/mongodb/models/User'; +import { TrainingLog } from '../server/mongodb/models/TrainingLog'; +import { Animal } from '../server/mongodb/models/Animal'; + +async function globalSetup(config: FullConfig) { + // clear the database and seed with users + try { + await connectDB(); + await User.collection.drop(); + await TrainingLog.collection.drop(); + await Animal.collection.drop(); + } catch (error) { + console.error('Unable to reset the database'); + } + console.log('URL ', `${config.webServer?.url}/api/user/register`); + const requestCtx = await request.newContext(); + const response = await requestCtx.post( + `${config.webServer?.url}/api/user/register`, + { + data: { + fullName: 'Bob Johnson', + email: 'alice3@gatech.edu', + password: '12345678', + confirmPassword: '12345678', + admin: true, + }, + } + ); + + console.log('Response ', response); +} + +export default globalSetup; diff --git a/e2e/train-logs.spec.ts b/e2e/train-logs.spec.ts new file mode 100644 index 0000000..f16edd4 --- /dev/null +++ b/e2e/train-logs.spec.ts @@ -0,0 +1,99 @@ +import test, { expect } from '@playwright/test'; +test.describe.serial('training log tests ', () => { + test.beforeAll(async ({ request, page }) => { + await page.goto('/'); + // Expect a title "to contain" a substring. + await page.getByRole('link', { name: 'app logo Progress' }).click(); + await page.getByRole('link', { name: 'Log In' }).click(); + await page.getByRole('textbox', { name: 'Email' }).click(); + await page + .getByRole('textbox', { name: 'Email' }) + .fill('alice3@gatech.edu'); + await page.getByRole('textbox', { name: 'Email' }).press('Tab'); + await page.getByRole('textbox', { name: 'Password' }).fill('12345678'); + await page.getByRole('button', { name: 'Log In' }).click(); + await page.getByRole('link', { name: 'Animals icon Animals' }).click(); + await page + .getByRole('link', { name: 'create new animal Create New' }) + .click(); + await page.getByRole('textbox', { name: 'Animal Name' }).click(); + await page.getByRole('textbox', { name: 'Animal Name' }).fill('Rover'); + await page.getByLabel('Breed').selectOption('German Shepherd'); + await page + .getByRole('spinbutton', { name: 'Total hours trained' }) + .fill('5'); + + await page.getByRole('button', { name: 'Save' }).click(); + await expect( + page.getByText('Animal created successfully!') + ).toBeVisible(); + }); + + test('create a new training log', async ({ page }) => { + await page.goto('/'); + // Expect a title "to contain" a substring. + await page.getByRole('link', { name: 'app logo Progress' }).click(); + await page.getByRole('link', { name: 'Log In' }).click(); + await page.getByRole('textbox', { name: 'Email' }).click(); + await page + .getByRole('textbox', { name: 'Email' }) + .fill('alice3@gatech.edu'); + await page.getByRole('textbox', { name: 'Email' }).press('Tab'); + await page.getByRole('textbox', { name: 'Password' }).fill('12345678'); + await page.getByRole('button', { name: 'Log In' }).click(); + await page + .getByRole('link', { name: 'create new training log' }) + .click(); + await page.getByRole('textbox', { name: 'Title' }).click(); + await page + .getByRole('textbox', { name: 'Title' }) + .fill('Fetch Practice'); + + await page.getByLabel('Select Animal').selectOption({ label: 'Rover' }); + await page.getByRole('textbox', { name: 'Description' }).click(); + await page + .getByRole('textbox', { name: 'Description' }) + .fill('Rover played fetch and returned the ball every time.'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.getByText('Training log created')).toBeVisible(); + + await page + .getByRole('link', { name: 'Training Logs icon Training' }) + .click(); + await expect(page.getByRole('main')).toContainText( + 'Rover played fetch and returned the ball every time.' + ); + }); + + test('edit a training log', async ({ page }) => { + await page.goto('/'); + // Expect a title "to contain" a substring. + await page.getByRole('link', { name: 'app logo Progress' }).click(); + await page.getByRole('link', { name: 'Log In' }).click(); + await page.getByRole('textbox', { name: 'Email' }).click(); + await page + .getByRole('textbox', { name: 'Email' }) + .fill('alice3@gatech.edu'); + await page.getByRole('textbox', { name: 'Email' }).press('Tab'); + await page.getByRole('textbox', { name: 'Password' }).fill('12345678'); + await page.getByRole('button', { name: 'Log In' }).click(); + + const dataLoaded = page.waitForRequest('/api/training-log/*'); + await page.getByRole('link', { name: 'edit training log' }).click(); + + await dataLoaded; + await page.getByRole('textbox', { name: 'Title' }).click(); + await page.waitForTimeout(5000); + await page + .getByRole('textbox', { name: 'Title' }) + .fill('Running Practice'); + await page.getByRole('button', { name: 'Save' }).click(); + await expect(page.locator('[id="1"]')).toContainText( + 'Training log updated successfully.' + ); + await page + .getByRole('link', { name: 'Training Logs icon Training' }) + .click(); + await expect(page.locator('h3')).toContainText('Running Practice'); + }); +}); diff --git a/playwright.config.ts b/playwright.config.ts index 1ca9b0a..2adc1a1 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -1,81 +1,83 @@ -import { defineConfig, devices } from "@playwright/test"; +import { defineConfig, devices } from '@playwright/test'; /** * Read environment variables from file. * https://github.com/motdotla/dotenv */ -// import dotenv from 'dotenv'; -// import path from 'path'; -// dotenv.config({ path: path.resolve(__dirname, '.env') }); - - +import dotenv from 'dotenv'; +import path from 'path'; +dotenv.config({ path: path.resolve(__dirname, '.env.local') }); /** * See https://playwright.dev/docs/test-configuration. */ export default defineConfig({ - testDir: './e2e', - /* Run tests in files in parallel */ - fullyParallel: true, - /* Fail the build on CI if you accidentally left test.only in the source code. */ - forbidOnly: !!process.env.CI, - /* Retry on CI only */ - retries: process.env.CI ? 2 : 0, - /* Opt out of parallel tests on CI. */ - workers: process.env.CI ? 1 : undefined, - /* Reporter to use. See https://playwright.dev/docs/test-reporters */ - reporter: 'html', - - /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ - use: { - screenshot: 'on-first-failure', - /* Base URL to use in actions like `await page.goto('')`. */ - baseURL: 'http://localhost:3000', - /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ - trace: 'on-first-retry', - }, + testDir: './e2e', + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: process.env.CI ? 2 : 0, + /* Opt out of parallel tests on CI. */ + workers: process.env.CI ? 1 : undefined, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'html', + globalSetup: './e2e/global-setup.ts', - /* Configure projects for major browsers */ - projects: [ - { - name: 'chromium', - use: { ...devices['Desktop Chrome'] }, + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + screenshot: 'on-first-failure', + /* Base URL to use in actions like `await page.goto('')`. */ + baseURL: 'http://localhost:3000', + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + launchOptions: { + slowMo: 100, + }, }, - // { - // name: 'firefox', - // use: { ...devices['Desktop Firefox'] }, - // }, + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { ...devices['Desktop Chrome'] }, + }, - // { - // name: 'webkit', - // use: { ...devices['Desktop Safari'] }, - // }, + // { + // name: 'firefox', + // use: { ...devices['Desktop Firefox'] }, + // }, - /* Test against mobile viewports. */ - // { - // name: 'Mobile Chrome', - // use: { ...devices['Pixel 5'] }, - // }, - // { - // name: 'Mobile Safari', - // use: { ...devices['iPhone 12'] }, - // }, + // { + // name: 'webkit', + // use: { ...devices['Desktop Safari'] }, + // }, - /* Test against branded browsers. */ - // { - // name: 'Microsoft Edge', - // use: { ...devices['Desktop Edge'], channel: 'msedge' }, - // }, - // { - // name: 'Google Chrome', - // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, - // }, - ], + /* Test against mobile viewports. */ + // { + // name: 'Mobile Chrome', + // use: { ...devices['Pixel 5'] }, + // }, + // { + // name: 'Mobile Safari', + // use: { ...devices['iPhone 12'] }, + // }, - /* Run your local dev server before starting the tests */ - webServer: { - command: 'npm run dev', - url: 'http://localhost:3000', - }, + /* Test against branded browsers. */ + // { + // name: 'Microsoft Edge', + // use: { ...devices['Desktop Edge'], channel: 'msedge' }, + // }, + // { + // name: 'Google Chrome', + // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, + // }, + ], + + /* Run your local dev server before starting the tests */ + webServer: { + command: 'npm run dev', + url: 'http://localhost:3000', + }, }); From 7c41b3a90caa8b89f36109e41cdcbeb1c6ee056f Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 13:34:04 -0500 Subject: [PATCH 11/19] feat(ui): add notification for successful animal creation --- src/app/dashboard/admin/animals/page.tsx | 28 +----- src/app/dashboard/animals/page.tsx | 2 + src/components/AnimalForm.tsx | 104 ----------------------- src/components/EditAnimalForm.tsx | 88 +++++++++---------- 4 files changed, 46 insertions(+), 176 deletions(-) delete mode 100644 src/components/AnimalForm.tsx diff --git a/src/app/dashboard/admin/animals/page.tsx b/src/app/dashboard/admin/animals/page.tsx index 64b0686..bd0a3ee 100644 --- a/src/app/dashboard/admin/animals/page.tsx +++ b/src/app/dashboard/admin/animals/page.tsx @@ -1,36 +1,12 @@ 'use client'; -import Link from 'next/link'; -import Image from 'next/image'; import { AnimalDocument } from '../../../../../server/mongodb/models/Animal'; import { useEffect, useState } from 'react'; import AnimalsGrid from '@/components/AnimalsGrid'; -type LogData = { - _id: string; - user: { _id: string; fullName: string }; - name: string; - animal_name: string; - breed: string; - hours: number; - url: string; -}; - -const mapDbAnimalToLogData = (dbAnimal: any): LogData => ({ - _id: dbAnimal._id, - user: { - _id: dbAnimal.owner._id, - fullName: dbAnimal.owner.fullName - }, - name: dbAnimal.name, - animal_name: dbAnimal.name, - breed: dbAnimal.breed, - hours: dbAnimal.hoursTrained, - url: dbAnimal.profilePicture, -}); export default function AdminAnimalsPage() { const [isLoading, setIsLoading] = useState(true); - const [animals, setAnimals] = useState([]); + const [animals, setAnimals] = useState([]); useEffect(() => { const fetchAnimals = async () => { @@ -43,7 +19,7 @@ export default function AdminAnimalsPage() { const animals: AnimalDocument[] = respBody?.data; if (animals != undefined) { - setAnimals(respBody?.data?.map(mapDbAnimalToLogData) ?? []); + setAnimals(animals); } setIsLoading(false); } catch (error) { diff --git a/src/app/dashboard/animals/page.tsx b/src/app/dashboard/animals/page.tsx index 9190d09..10c9c9c 100644 --- a/src/app/dashboard/animals/page.tsx +++ b/src/app/dashboard/animals/page.tsx @@ -5,6 +5,7 @@ import AnimalCard from '@/components/AnimalCard'; import { useEffect, useState } from 'react'; import { AnimalDocument } from '../../../../server/mongodb/models/Animal'; import AnimalsGrid from '@/components/AnimalsGrid'; +import { ToastContainer } from 'react-toastify'; export default function AnimalsPage() { const [isLoading, setIsLoading] = useState(true); @@ -38,6 +39,7 @@ export default function AnimalsPage() {

Animals

+
{ -// const params = useParams(); -// const {id} = params; -// const router = useRouter(); -// const { register, handleSubmit, formState: { errors }} = useForm() - -// const onSubmit: SubmitHandler = (data) => { - -// router.push('/dashboard') - -// } -// return ( -//
-// -// -// -//
-//
-// -//
-//
-// -//
- -//
-// -//
- -//
-//
-// -//
-//
-// {/* */} -// {/* */} -//
- - -//
-// ) -// } -// export default AnimalForm \ No newline at end of file diff --git a/src/components/EditAnimalForm.tsx b/src/components/EditAnimalForm.tsx index 976b5c8..97c3ff5 100644 --- a/src/components/EditAnimalForm.tsx +++ b/src/components/EditAnimalForm.tsx @@ -39,30 +39,28 @@ const EditAnimalForm = () => { if (id) { // edit existing training log try { - const response = await fetch(`/api/animal/${id}`, { - method: 'PATCH', - credentials: 'include', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(data), - }); + const response = await fetch(`/api/animal/${id}`, { + method: 'PATCH', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); - const respBody = await response.json(); + const respBody = await response.json(); - if (!respBody.data) { - setError('root.serverError', { - message: respBody.error, - }); - return; - } + if (!respBody.data) { + setError('root.serverError', { + message: respBody.error, + }); + return; + } - toast('Animal updated successfully!'); - router.push('/dashboard/animals'); + toast('Animal updated successfully!'); } catch (err) { - setError('root.serverError', { - message: 'Failed to update animal', - }); + setError('root.serverError', { + message: 'Failed to update animal', + }); } - } else { //create new training log try { @@ -79,7 +77,6 @@ const EditAnimalForm = () => { }); } toast('Animal created successfully!'); - router.push('/dashboard/animals'); reset(); } catch (err) { console.log; @@ -94,25 +91,25 @@ const EditAnimalForm = () => { if (!id) return; const fetchAnimal = async () => { - try { - const response = await fetch(`/api/animal/${id}`, { - method: 'GET', - credentials: 'include', - }); - const respBody = await response.json(); - const animal = respBody.data; + try { + const response = await fetch(`/api/animal/${id}`, { + method: 'GET', + credentials: 'include', + }); + const respBody = await response.json(); + const animal = respBody.data; - if (animal) { - reset({ - name: animal.name, - breed: animal.breed, - hoursTrained: animal.hoursTrained, - profilePicture: animal.profilePicture, - }); + if (animal) { + reset({ + name: animal.name, + breed: animal.breed, + hoursTrained: animal.hoursTrained, + profilePicture: animal.profilePicture, + }); + } + } catch (err) { + console.error('Failed to load animal:', err); } - } catch (err) { - console.error('Failed to load animal:', err); - } }; fetchAnimal(); @@ -125,11 +122,11 @@ const EditAnimalForm = () => { {errors?.root?.serverError.message}

)} + -
{ From 65a8fc6ce17360b154a9a256a8016ca5aa1c63c8 Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 13:43:39 -0500 Subject: [PATCH 12/19] add logging statement to debug connection failure --- e2e/global-setup.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index 386481e..a5d5623 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -7,6 +7,7 @@ import { Animal } from '../server/mongodb/models/Animal'; async function globalSetup(config: FullConfig) { // clear the database and seed with users try { + console.log(process.env.MONGO_DB); await connectDB(); await User.collection.drop(); await TrainingLog.collection.drop(); From f4b15eea081ef2da8a15c84720d894cd3234c6e9 Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 13:47:01 -0500 Subject: [PATCH 13/19] add debug stmt --- e2e/global-setup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index a5d5623..85c6ca9 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -6,8 +6,8 @@ import { Animal } from '../server/mongodb/models/Animal'; async function globalSetup(config: FullConfig) { // clear the database and seed with users + console.log('MONGO_DB ', process.env.MONGO_DB); try { - console.log(process.env.MONGO_DB); await connectDB(); await User.collection.drop(); await TrainingLog.collection.drop(); From 2edf18c93c6ebf0e54d5b578d5933585f7565790 Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 13:50:12 -0500 Subject: [PATCH 14/19] add debug stmts --- .github/workflows/playwright.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 032d1e8..4b968ee 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -14,6 +14,8 @@ jobs: timeout-minutes: 60 runs-on: ubuntu-latest steps: + - name: Print Environment Variables + run: printenv - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: From 8959fc4188f17f78c1511ba7945c30a46aacfdb7 Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 13:52:11 -0500 Subject: [PATCH 15/19] Remove later steps to debug missing env --- .github/workflows/playwright.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 4b968ee..fda0284 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -16,19 +16,19 @@ jobs: steps: - name: Print Environment Variables run: printenv - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 - with: - node-version: lts/* - - name: Install dependencies - run: npm ci - - name: Install Playwright Browsers - run: npx playwright install --with-deps - - name: Run Playwright tests - run: npx playwright test - - uses: actions/upload-artifact@v4 - if: ${{ !cancelled() }} - with: - name: playwright-report - path: playwright-report/ - retention-days: 30 + # - uses: actions/checkout@v4 + # - uses: actions/setup-node@v4 + # with: + # node-version: lts/* + # - name: Install dependencies + # run: npm ci + # - name: Install Playwright Browsers + # run: npx playwright install --with-deps + # - name: Run Playwright tests + # run: npx playwright test + # - uses: actions/upload-artifact@v4 + # if: ${{ !cancelled() }} + # with: + # name: playwright-report + # path: playwright-report/ + # retention-days: 30 From 9b6e5f98b967799dcc673583e01022bb05beb9c5 Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 13:56:03 -0500 Subject: [PATCH 16/19] feat(ci): add env to pipeline --- .github/workflows/playwright.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index fda0284..7421848 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -11,6 +11,7 @@ on: branches: [main, master] jobs: test: + environment: dev timeout-minutes: 60 runs-on: ubuntu-latest steps: From 4dfbb63e0a509070f45480abfbc6e23ab2a00192 Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 13:57:18 -0500 Subject: [PATCH 17/19] tests: add env to pipeline --- .github/workflows/playwright.yml | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index 7421848..27158f1 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -17,19 +17,19 @@ jobs: steps: - name: Print Environment Variables run: printenv - # - uses: actions/checkout@v4 - # - uses: actions/setup-node@v4 - # with: - # node-version: lts/* - # - name: Install dependencies - # run: npm ci - # - name: Install Playwright Browsers - # run: npx playwright install --with-deps - # - name: Run Playwright tests - # run: npx playwright test - # - uses: actions/upload-artifact@v4 - # if: ${{ !cancelled() }} - # with: - # name: playwright-report - # path: playwright-report/ - # retention-days: 30 + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: lts/* + - name: Install dependencies + run: npm ci + - name: Install Playwright Browsers + run: npx playwright install --with-deps + - name: Run Playwright tests + run: npx playwright test + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-report + path: playwright-report/ + retention-days: 30 From 125694e1a8e20ef083b77f2f7e9cf06171650225 Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 13:57:38 -0500 Subject: [PATCH 18/19] remove log stmt --- e2e/global-setup.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/e2e/global-setup.ts b/e2e/global-setup.ts index 85c6ca9..386481e 100644 --- a/e2e/global-setup.ts +++ b/e2e/global-setup.ts @@ -6,7 +6,6 @@ import { Animal } from '../server/mongodb/models/Animal'; async function globalSetup(config: FullConfig) { // clear the database and seed with users - console.log('MONGO_DB ', process.env.MONGO_DB); try { await connectDB(); await User.collection.drop(); From 1f7feb82adac43e05a647d355d2ab432e428e263 Mon Sep 17 00:00:00 2001 From: Keshav Worathur Date: Thu, 20 Nov 2025 14:00:44 -0500 Subject: [PATCH 19/19] tests: fix bug in test setup step --- e2e/train-logs.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/e2e/train-logs.spec.ts b/e2e/train-logs.spec.ts index f16edd4..f026820 100644 --- a/e2e/train-logs.spec.ts +++ b/e2e/train-logs.spec.ts @@ -1,6 +1,7 @@ import test, { expect } from '@playwright/test'; test.describe.serial('training log tests ', () => { - test.beforeAll(async ({ request, page }) => { + test.beforeAll(async ({ browser }) => { + const page = await browser.newPage(); await page.goto('/'); // Expect a title "to contain" a substring. await page.getByRole('link', { name: 'app logo Progress' }).click();