Skip to content
Merged
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
28 changes: 24 additions & 4 deletions client/src/contexts/AuthContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,31 @@ export const AuthProvider = ({ children }: { children: ReactNode }) => {

useEffect(() => {
// Check if user is already logged in
const currentUser = api.getCurrentUser();
if (currentUser) {
setUser(currentUser);
const token = localStorage.getItem('auth_token');
if (token) {
// Fetch user from server to get fresh data
api
.getMe()
.then((response) => {
if (response.user) {
setUser(response.user);
// Update localStorage with fresh user data
localStorage.setItem('user', JSON.stringify(response.user));
}
})
.catch(() => {
// If getMe fails, try localStorage fallback
const currentUser = api.getCurrentUser();
if (currentUser) {
setUser(currentUser);
}
})
.finally(() => {
setLoading(false);
});
} else {
setLoading(false);
}
setLoading(false);
}, []);

const login = async (email: string, password: string) => {
Expand Down
126 changes: 126 additions & 0 deletions client/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -535,6 +535,132 @@ footer {
text-decoration: underline;
}

.profile-page {
max-width: 1100px;
}

.profile-page .page-heading-row {
align-items: center;
}

.profile-badge {
background: rgba(27, 47, 86, 0.9) !important;
border: 1px solid rgba(255, 227, 169, 0.5);
}

.profile-card {
overflow: hidden;
}

.profile-loading-text {
color: #1b2f56;
font-weight: 700;
}

.profile-avatar {
width: 96px;
height: 96px;
border-radius: 50%;
margin: 0 auto;
display: grid;
place-items: center;
font-size: 34px;
font-weight: 800;
color: #f2dca0;
background: linear-gradient(135deg, #1b2f56, #172646);
border: 2px solid #c8912e;
box-shadow:
0 10px 22px rgba(0, 0, 0, 0.14),
0 0 0 1px rgba(255, 227, 169, 0.4);
}

.profile-avatar--guest {
background: linear-gradient(135deg, #7a849a, #636f88);
}

.profile-title {
color: #1b2f56;
font-weight: 800;
margin-bottom: 6px;
}

.profile-subtitle {
color: #1f2a44;
font-weight: 600;
}

.profile-email-pill {
margin: 14px auto 0;
max-width: 100%;
display: inline-flex;
padding: 8px 14px;
border-radius: 999px;
background: rgba(255, 255, 255, 0.82);
border: 1px solid rgba(200, 145, 46, 0.45);
color: #1f2a44;
font-weight: 700;
}

.profile-status-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: #28a745;
box-shadow: 0 0 8px rgba(40, 167, 69, 0.5);
}

.profile-status-text {
color: #1f2a44;
font-weight: 700;
font-size: 16px;
}

.profile-section-title {
color: #1b2f56;
font-size: 22px;
font-weight: 800;
}

.profile-info-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}

.profile-info-item {
background: rgba(255, 255, 255, 0.76);
border: 1px solid rgba(200, 145, 46, 0.4);
border-radius: 12px;
padding: 12px 14px;
}

.profile-info-label {
display: block;
color: #5e6472;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.6px;
font-weight: 700;
}

.profile-info-value {
display: block;
margin-top: 2px;
color: #1f2a44;
font-size: 18px;
}

@media (max-width: 768px) {
.profile-info-grid {
grid-template-columns: 1fr;
}

.profile-section-title {
font-size: 20px;
}
}

@media (max-width: 576px) {
.contact-card__name {
font-size: 22px;
Expand Down
30 changes: 20 additions & 10 deletions client/src/services/recipeSuggestionService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -336,7 +336,7 @@ function buildRecipe(
export async function getRecipesFromInventory(
inventoryArray: string[],
inventoryUnitHints: Array<{ name: string; unit: string }> = [],
cuisine: CuisineType = DEFAULT_CUISINE
cuisine?: CuisineType
): Promise<RecipeResponse> {
if (!Array.isArray(inventoryArray) || inventoryArray.length === 0) {
throw new Error('Please add ingredients to your inventory first.');
Expand All @@ -351,8 +351,10 @@ export async function getRecipesFromInventory(
}

const preferredUnits = preferredUnitMap(inventoryUnitHints);
const normalizedCuisine = normalizeCuisine(cuisine, DEFAULT_CUISINE) ?? DEFAULT_CUISINE;
const cuisineLabel = CUISINE_OPTIONS.find((option) => option.value === normalizedCuisine)?.label ?? normalizedCuisine;
const normalizedCuisine = normalizeCuisine(cuisine);
const randomCuisineGuide = CUISINE_OPTIONS.map((option) => option.value).join(', ');
const fallbackCuisine =
normalizedCuisine ?? CUISINE_OPTIONS[Math.floor(Math.random() * CUISINE_OPTIONS.length)]?.value ?? DEFAULT_CUISINE;

const ingredients = cleanedInventory.join(', ');
const unitHintText = inventoryUnitHints
Expand All @@ -367,7 +369,11 @@ export async function getRecipesFromInventory(
const prompt = `I have these ingredients: ${ingredients}.
Role: You are a Culinary Data Engineer Agent.

Task: Based on the provided ingredient list, suggest 3 distinct recipes classified as quick, healthy, and surprise with a ${cuisineLabel} cuisine focus.
Task: Based on the provided ingredient list, suggest 3 distinct recipes classified as quick, healthy, and surprise${
normalizedCuisine
? ` with a ${CUISINE_OPTIONS.find((option) => option.value === normalizedCuisine)?.label ?? normalizedCuisine} cuisine focus`
: ` with random cuisine inspiration chosen from ${randomCuisineGuide}`
}.

Core Logic Rules:
- The Single-Person Rule is mandatory: calculate all ingredient measurements for exactly one person.
Expand All @@ -378,14 +384,18 @@ Core Logic Rules:
- Pantry matching is mandatory: use only provided inventory ingredients, but common staples (salt, water, oil) are allowed.
- Structure is mandatory: each recipe must have exactly 3 concise steps.
- Instruction text may use user-friendly wording and convenient kitchen measurements for readability.
- Set the cuisine field on every recipe to "${normalizedCuisine}".
- Set the cuisine field on every recipe.${
normalizedCuisine
? ` Use exactly "${normalizedCuisine}".`
: ` Choose one cuisine value from: ${randomCuisineGuide}.`
}

Respond with ONLY valid JSON in this exact structure:
{
"recipes": [
{
"type": "quick",
"cuisine": "${normalizedCuisine}",
"cuisine": "${normalizedCuisine ?? fallbackCuisine}",
"title": "",
"description": "",
"preparationTime": "15 mins",
Expand All @@ -397,7 +407,7 @@ Respond with ONLY valid JSON in this exact structure:
},
{
"type": "healthy",
"cuisine": "${normalizedCuisine}",
"cuisine": "${normalizedCuisine ?? fallbackCuisine}",
"title": "",
"description": "",
"preparationTime": "20 mins",
Expand All @@ -409,7 +419,7 @@ Respond with ONLY valid JSON in this exact structure:
},
{
"type": "surprise",
"cuisine": "${normalizedCuisine}",
"cuisine": "${normalizedCuisine ?? fallbackCuisine}",
"title": "",
"description": "",
"preparationTime": "25 mins",
Expand Down Expand Up @@ -452,7 +462,7 @@ Respond with ONLY valid JSON in this exact structure:

recipeByType.set(type, {
type,
cuisine: normalizeCuisine(match.cuisine, normalizedCuisine) ?? normalizedCuisine,
cuisine: normalizeCuisine(match.cuisine, fallbackCuisine) ?? fallbackCuisine,
title: String(match.title ?? `${type} recipe`).trim() || `${type} recipe`,
description: String(match.description ?? '').trim() || `${type} recipe for one person.`,
preparationTime: String(match.preparationTime ?? '20 mins').trim() || '20 mins',
Expand All @@ -466,7 +476,7 @@ Respond with ONLY valid JSON in this exact structure:
}

const recipes = RECIPE_TYPES.map(
(type) => recipeByType.get(type) ?? buildRecipe(type, cleanedInventory, preferredUnits, normalizedCuisine)
(type) => recipeByType.get(type) ?? buildRecipe(type, cleanedInventory, preferredUnits, fallbackCuisine)
);
return { recipes };
} catch (error: unknown) {
Expand Down
Loading
Loading