diff --git a/.github/workflows/proj3-deploy.yml b/.github/workflows/proj3-deploy.yml new file mode 100644 index 000000000..1ce9f1e73 --- /dev/null +++ b/.github/workflows/proj3-deploy.yml @@ -0,0 +1,54 @@ +name: Proj3 Deploy (manual) + +on: + workflow_dispatch: + inputs: + environment: + description: "Target environment name" + default: "production" + required: true + +jobs: + build-and-push-backend: + name: Proj3 • Build & Push Backend Image (GHCR) + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build & Push + uses: docker/build-push-action@v6 + with: + context: proj3/backend + push: true + tags: ghcr.io/${{ github.repository }}-proj3-backend:latest,ghcr.io/${{ github.repository }}-proj3-backend:${{ github.sha }} + + build-and-push-frontend: + name: Proj3 • Build & Push Frontend Image (GHCR) + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build & Push + uses: docker/build-push-action@v6 + with: + context: proj3/frontend + push: true + tags: ghcr.io/${{ github.repository }}-proj3-frontend:latest,ghcr.io/${{ github.repository }}-proj3-frontend:${{ github.sha }} diff --git a/proj3/backend/app/main.py b/proj3/backend/app/main.py index aff7e85d7..2f89f5ee2 100644 --- a/proj3/backend/app/main.py +++ b/proj3/backend/app/main.py @@ -29,6 +29,7 @@ async def lifespan(app: FastAPI): lifespan=lifespan, ) +# This is a test comment to trigger the workflow frontend_path = os.path.abspath( os.path.join(os.path.dirname(__file__), "..", "frontend", "dist") ) diff --git a/proj3/frontend/src/components/CartTab.jsx b/proj3/frontend/src/components/CartTab.jsx index f63338052..0bc0d1800 100644 --- a/proj3/frontend/src/components/CartTab.jsx +++ b/proj3/frontend/src/components/CartTab.jsx @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ import React from "react"; import { Button } from './ui/button'; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from './ui/alert-dialog'; diff --git a/proj3/frontend/src/components/Dashboard.jsx b/proj3/frontend/src/components/Dashboard.jsx index a271ce5ea..ff8598df4 100644 --- a/proj3/frontend/src/components/Dashboard.jsx +++ b/proj3/frontend/src/components/Dashboard.jsx @@ -143,7 +143,7 @@ export default function Dashboard({ onLogout }) { })); } } - }, [user]); + }, [user, preferences.userLocation]); useEffect(() => { localStorage.setItem("preferences", JSON.stringify(preferences)); diff --git a/proj3/frontend/src/components/DisputeForm.jsx b/proj3/frontend/src/components/DisputeForm.jsx index a851ac545..27f03124f 100644 --- a/proj3/frontend/src/components/DisputeForm.jsx +++ b/proj3/frontend/src/components/DisputeForm.jsx @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ import React, { useState } from 'react'; import { Button } from './ui/button'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'; diff --git a/proj3/frontend/src/components/DisputesTab.jsx b/proj3/frontend/src/components/DisputesTab.jsx index 2f3a69078..26afae5f0 100644 --- a/proj3/frontend/src/components/DisputesTab.jsx +++ b/proj3/frontend/src/components/DisputesTab.jsx @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ import React, { useState, useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card'; import { Badge } from './ui/badge'; diff --git a/proj3/frontend/src/components/EventsTab.jsx b/proj3/frontend/src/components/EventsTab.jsx index c3891625f..6002cda94 100644 --- a/proj3/frontend/src/components/EventsTab.jsx +++ b/proj3/frontend/src/components/EventsTab.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState, useCallback } from 'react'; import { listEvents, listMyEvents, createEvent, joinEvent, addDish } from '../services/EventService'; import { Button } from './ui/button'; import { Card, CardHeader, CardTitle, CardContent } from './ui/card'; @@ -16,7 +16,7 @@ export default function EventsTab({ currentUser, authToken }) { const [dishTitle, setDishTitle] = useState(''); const [dishDescription, setDishDescription] = useState(''); - const load = async (mine = false) => { + const load = useCallback(async (mine = false) => { try { let data; if (mine) { @@ -34,9 +34,9 @@ export default function EventsTab({ currentUser, authToken }) { console.error('Failed to load events', err); toast.error('Failed to load events'); } - }; + }, [authToken]); - useEffect(() => { load(showMine); }, [showMine]); + useEffect(() => { load(showMine); }, [showMine, load]); const handleCreate = async () => { if (!form.title || !form.date) { diff --git a/proj3/frontend/src/components/MealCard.jsx b/proj3/frontend/src/components/MealCard.jsx index d1cd642e5..b4315ca7a 100644 --- a/proj3/frontend/src/components/MealCard.jsx +++ b/proj3/frontend/src/components/MealCard.jsx @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ import { useState } from "react"; import { Card, CardHeader, CardTitle, CardDescription, CardContent } from "./ui/card"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from './ui/alert-dialog'; diff --git a/proj3/frontend/src/components/OrderHistoryTab.jsx b/proj3/frontend/src/components/OrderHistoryTab.jsx index 294ababa8..3d370aa75 100644 --- a/proj3/frontend/src/components/OrderHistoryTab.jsx +++ b/proj3/frontend/src/components/OrderHistoryTab.jsx @@ -1,4 +1,3 @@ -/* eslint-disable no-unused-vars */ import React, { useState, useEffect } from 'react'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card'; import { Badge } from './ui/badge'; diff --git a/proj3/frontend/src/components/Profile.jsx b/proj3/frontend/src/components/Profile.jsx index 790097ff0..a5da38394 100644 --- a/proj3/frontend/src/components/Profile.jsx +++ b/proj3/frontend/src/components/Profile.jsx @@ -1,4 +1,3 @@ -/* eslint-disable no-undef */ import { useState, useEffect } from "react"; import * as Dialog from "@radix-ui/react-dialog"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; // adjust your import diff --git a/proj3/frontend/src/components/RecommendationsTab.jsx b/proj3/frontend/src/components/RecommendationsTab.jsx index 61e1f4621..b102d1d39 100644 --- a/proj3/frontend/src/components/RecommendationsTab.jsx +++ b/proj3/frontend/src/components/RecommendationsTab.jsx @@ -1,5 +1,3 @@ -/* eslint-disable no-undef */ -/* eslint-disable no-unused-vars */ import { useEffect, useState } from 'react'; import { getRecommendedMeals, getAllMeals } from '../services/MealService'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from './ui/card'; diff --git a/proj3/frontend/src/components/ReviewsList.jsx b/proj3/frontend/src/components/ReviewsList.jsx index 67d9bec83..c689cf968 100644 --- a/proj3/frontend/src/components/ReviewsList.jsx +++ b/proj3/frontend/src/components/ReviewsList.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useCallback } from 'react'; import StarRating from './StarRating'; const ReviewsList = ({ userId }) => { @@ -6,11 +6,7 @@ const ReviewsList = ({ userId }) => { const [loading, setLoading] = useState(true); const [error, setError] = useState(''); - useEffect(() => { - fetchReviews(); - }, [userId]); - - const fetchReviews = async () => { + const fetchReviews = useCallback(async () => { try { const response = await fetch(`http://localhost:8000/reviews/user/${userId}`); if (!response.ok) { @@ -23,7 +19,11 @@ const ReviewsList = ({ userId }) => { } finally { setLoading(false); } - }; + }, [userId]); + + useEffect(() => { + fetchReviews(); + }, [fetchReviews]); if (loading) { return ( diff --git a/proj3/frontend/src/components/ui/badge.jsx b/proj3/frontend/src/components/ui/badge.jsx index d725ae33d..d53295e71 100644 --- a/proj3/frontend/src/components/ui/badge.jsx +++ b/proj3/frontend/src/components/ui/badge.jsx @@ -1,30 +1,9 @@ import * as React from "react" import { Slot } from "@radix-ui/react-slot" -import { cva } from "class-variance-authority"; +import { badgeVariants } from "./variants" import { cn } from "@/lib/utils" -const badgeVariants = cva( - "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", - { - variants: { - variant: { - default: - "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", - secondary: - "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", - destructive: - "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - } -) - function Badge({ className, variant, @@ -41,4 +20,4 @@ function Badge({ ); } -export { Badge, badgeVariants } +export { Badge } diff --git a/proj3/frontend/src/components/ui/button.jsx b/proj3/frontend/src/components/ui/button.jsx index aa6f4cb81..ed23890b0 100644 --- a/proj3/frontend/src/components/ui/button.jsx +++ b/proj3/frontend/src/components/ui/button.jsx @@ -1,41 +1,9 @@ import * as React from "react" import { Slot } from "@radix-ui/react-slot" -import { cva } from "class-variance-authority"; +import { buttonVariants } from "./variants" import { cn } from "@/lib/utils" -const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", - outline: - "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: - "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-sm": "size-8", - "icon-lg": "size-10", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - } -) - function Button({ className, variant, @@ -53,4 +21,4 @@ function Button({ ); } -export { Button, buttonVariants } +export { Button } diff --git a/proj3/frontend/src/components/ui/variants.js b/proj3/frontend/src/components/ui/variants.js new file mode 100644 index 000000000..3ddba713e --- /dev/null +++ b/proj3/frontend/src/components/ui/variants.js @@ -0,0 +1,54 @@ +import { cva } from "class-variance-authority"; + +export const badgeVariants = cva( + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", + secondary: + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", + destructive: + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +); + +export const buttonVariants = cva( + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: + "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: "h-9 px-4 py-2 has-[>svg]:px-3", + sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", + lg: "h-10 rounded-md px-6 has-[>svg]:px-4", + icon: "size-9", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +); diff --git a/proj3/frontend/src/services/MealService.jsx b/proj3/frontend/src/services/MealService.jsx index 77f041825..c9582c9cd 100644 --- a/proj3/frontend/src/services/MealService.jsx +++ b/proj3/frontend/src/services/MealService.jsx @@ -33,7 +33,7 @@ export const createMeal = async (mealData) => { let errorBody; try { errorBody = await response.json(); - } catch (e) { + } catch { throw new Error("Failed to create meal"); } diff --git a/proj3/frontend/src/utils/badges.js b/proj3/frontend/src/utils/badges.js index c27d2505f..35cf4ca19 100644 --- a/proj3/frontend/src/utils/badges.js +++ b/proj3/frontend/src/utils/badges.js @@ -218,14 +218,16 @@ export const getBadgeProgress = (badgeId, userStats, userMeals = []) => { return { current: totalReviews, required: 10, extraInfo: `${avgRating.toFixed(1)} rating` }; case 'community_favorite': return { current: totalReviews, required: 100 }; - case 'diverse_cook': + case 'diverse_cook': { const uniqueCuisines = new Set(userMeals.map(m => m.cuisine_type).filter(Boolean)); return { current: uniqueCuisines.size, required: 5 }; - case 'generous_sharer': + } + case 'generous_sharer': { const freeOrSwapMeals = userMeals.filter(m => (m.sale_price === 0 || !m.available_for_sale) && m.available_for_swap ); return { current: freeOrSwapMeals.length, required: 10 }; + } default: return null; }