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
5 changes: 4 additions & 1 deletion backend/config/cors.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

'allowed_methods' => ['*'],

'allowed_origins' => ['http://localhost:3000'],
'allowed_origins' => [
'http://localhost:3000',
'http://127.0.0.1:3000',
],

'allowed_origins_patterns' => [],

Expand Down
2 changes: 1 addition & 1 deletion backend/config/sanctum.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@

'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', sprintf(
'%s%s',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1',
'localhost,localhost:3000,127.0.0.1,127.0.0.1:3000,127.0.0.1:8000,::1',
Sanctum::currentApplicationUrlWithPort(),
// Sanctum::currentRequestHost(),
))),
Expand Down
38 changes: 36 additions & 2 deletions frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import type { Metadata } from "next";
import type { Metadata, Viewport } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { ToastProvider } from "@/components/providers/toast-provider";
import { LanguageProvider } from "@/components/providers/language-provider";
import { ThemeProvider } from "@/components/providers/theme-provider";
import { PwaRegister } from "@/components/pwa/pwa-register";
import { PwaInstallPrompt } from "@/components/pwa/pwa-install-prompt";
import { NetworkStatusIndicator } from "@/components/pwa/network-status-indicator";
import "./globals.css";

const geistSans = Geist({
Expand All @@ -15,9 +18,37 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});

export const viewport: Viewport = {
themeColor: [
{ media: "(prefers-color-scheme: light)", color: "#ffffff" },
{ media: "(prefers-color-scheme: dark)", color: "#1C1C1C" },
],
width: "device-width",
initialScale: 1,
maximumScale: 5,
};

export const metadata: Metadata = {
title: "MealBuddy",
title: "MealBuddy | Office Lunch Management",
description: "Office cafeteria lunch management and billing",
manifest: "/manifest.webmanifest",
appleWebApp: {
capable: true,
statusBarStyle: "default",
title: "MealBuddy | Office Lunch Management",
},
formatDetection: {
telephone: false,
},
icons: {
icon: [
{ url: "/icons/icon-192x192.png", sizes: "192x192", type: "image/png" },
{ url: "/icons/icon-512x512.png", sizes: "512x512", type: "image/png" },
],
apple: [
{ url: "/icons/apple-touch-icon.png", sizes: "180x180", type: "image/png" },
],
},
};

const themeScript = `
Expand Down Expand Up @@ -52,6 +83,9 @@ export default function RootLayout({
<ThemeProvider>
<LanguageProvider>
<ToastProvider>{children}</ToastProvider>
<NetworkStatusIndicator />
<PwaInstallPrompt />
<PwaRegister />
</LanguageProvider>
</ThemeProvider>
</body>
Expand Down
70 changes: 70 additions & 0 deletions frontend/app/manifest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { MetadataRoute } from 'next';

export default function manifest(): MetadataRoute.Manifest {
return {
name: 'MealBuddy | Office Lunch Management',
short_name: 'MealBuddy | Office Lunch Management',
description: 'Office cafeteria lunch management, daily meal voting, and billing',
start_url: '/',
scope: '/',
display: 'standalone',
orientation: 'portrait-primary',
background_color: '#ffffff',
theme_color: '#2E5A88',
icons: [
{
src: '/icons/icon-192x192.png',
sizes: '192x192',
type: 'image/png',
purpose: 'any',
},
{
src: '/icons/icon-512x512.png',
sizes: '512x512',
type: 'image/png',
purpose: 'any',
},
{
src: '/icons/icon-512x512-maskable.png',
sizes: '512x512',
type: 'image/png',
purpose: 'maskable',
},
{
src: '/icons/apple-touch-icon.png',
sizes: '180x180',
type: 'image/png',
},
],
shortcuts: [
{
name: "Today's Lunch / Vote",
short_name: 'Vote',
description: 'Cast your daily lunch vote before 10 AM',
url: '/vote',
icons: [{ src: '/icons/icon-192x192.png', sizes: '192x192' }],
},
{
name: 'My Dashboard',
short_name: 'Dashboard',
description: 'View lunch attendance and meal history',
url: '/user_dashboard',
icons: [{ src: '/icons/icon-192x192.png', sizes: '192x192' }],
},
{
name: 'Lunch Billing',
short_name: 'Billing',
description: 'View monthly cafeteria lunch billing',
url: '/user_dashboard/billing',
icons: [{ src: '/icons/icon-192x192.png', sizes: '192x192' }],
},
{
name: 'Chef Dashboard',
short_name: 'Chef',
description: 'Manage menus and headcount',
url: '/dashboard',
icons: [{ src: '/icons/icon-192x192.png', sizes: '192x192' }],
},
],
};
}
56 changes: 56 additions & 0 deletions frontend/app/offline/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
'use client';

import React from 'react';
import { WifiOff, RefreshCw, ArrowLeft } from 'lucide-react';
import { useLanguage } from '@/components/providers/language-provider';

export default function OfflinePage() {
const { t } = useLanguage();

const handleRetry = () => {
window.location.reload();
};

const handleGoBack = () => {
window.history.back();
};

return (
<main className="min-h-screen bg-background text-foreground flex flex-col items-center justify-center p-4 sm:p-6 text-center">
<div className="max-w-md w-full bg-card border border-border rounded-2xl p-6 sm:p-8 shadow-sm flex flex-col items-center animate-in fade-in zoom-in-95 duration-200">
<div className="w-16 h-16 rounded-full bg-destructive/10 text-destructive flex items-center justify-center mb-5">
<WifiOff className="w-8 h-8" />
</div>

<h1 className="text-2xl sm:text-3xl font-bold tracking-tight mb-3">
{t('you_are_offline')}
</h1>

<p className="text-muted-foreground text-sm sm:text-base mb-6 leading-relaxed">
{t('internet_connection_unavailable')}
</p>

<p className="text-xs text-muted-foreground/80 mb-6">
{t('offline_reconnect_prompt')}
</p>

<div className="flex flex-col sm:flex-row gap-3 w-full">
<button
onClick={handleRetry}
className="flex-1 inline-flex items-center justify-center gap-2 px-5 py-3 rounded-xl bg-primary text-primary-foreground font-medium hover:opacity-90 transition-opacity focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2"
>
<RefreshCw className="w-4 h-4" />
<span>{t('try_again')}</span>
</button>
<button
onClick={handleGoBack}
className="inline-flex items-center justify-center gap-2 px-4 py-3 rounded-xl border border-border bg-secondary text-secondary-foreground font-medium hover:bg-accent transition-colors"
>
<ArrowLeft className="w-4 h-4" />
<span>{t('back_to_website')}</span>
</button>
</div>
</div>
</main>
);
}
30 changes: 29 additions & 1 deletion frontend/components/providers/language-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,20 @@ const translations = {
dietary_preference: 'Dietary Preference',
veg: 'Vegetarian',
non_veg: 'Non-Vegetarian',

// PWA & Offline
install_mealbuddy: 'Install MealBuddy',
install_app_desc: 'Install MealBuddy on your device for quick access and a faster experience.',
add_to_home_screen: 'Add to Home Screen',
you_are_offline: "You're offline",
back_online: 'Back online',
try_again: 'Try Again',
new_version_available: 'New MealBuddy version available',
refresh: 'Refresh',
install_instructions_ios: 'To install: tap Share and select "Add to Home Screen"',
internet_connection_unavailable: 'Internet connection is unavailable. Live meal voting, menu updates, and billing cannot currently be loaded.',
offline_reconnect_prompt: 'Please reconnect to the internet and try again.',
dismiss: 'Dismiss',
},
bo: {
app_title: 'ཟས་མཐུན་ལས་རོགས།',
Expand Down Expand Up @@ -616,6 +630,20 @@ const translations = {
dietary_preference: 'དཀར་དམར་གྱི་འདེམས་ཀ།',
veg: 'དཀར་ཟས།',
non_veg: 'དམར་ཟས།',

// PWA & Offline
install_mealbuddy: 'MealBuddy གཏེགས་འཇུག་བྱོས།',
install_app_desc: 'མགྱོགས་མྱུར་སྤྱོད་བདེའི་ཆེད་ MealBuddy ཉེར་སྤྱོད་ཁྱེད་ཀྱི་ཁ་པར་ནང་གཏེགས་འཇུག་གནང་རོགས།',
add_to_home_screen: 'གཙོ་ངོས་སུ་སྣོན་པ།',
you_are_offline: 'དྲ་རྒྱའི་སྦྲེལ་མཐུད་མེད།',
back_online: 'དྲ་རྒྱའི་སྦྲེལ་མཐུད་སླར་གསོ་བྱུང་།',
try_again: 'བསྐྱར་དུ་འབད་བརྩོན་བྱོས།',
new_version_available: 'MealBuddy པར་གཞི་གསར་པ་འདུག',
refresh: 'གསར་བཅོས་བྱོས།',
install_instructions_ios: 'གཏེགས་འཇུག་བྱེད་པར། མཉམ་སྤྱོད་ (Share) མནན་ཏེ་ "གཙོ་ངོས་སུ་སྣོན་པ་" (Add to Home Screen) འདེམས་རོགས།',
internet_connection_unavailable: 'དྲ་རྒྱའི་སྦྲེལ་མཐུད་མི་འདུག ཉིན་ཟས་འོས་བསྡུ་དང་རྩིས་ཁྲ་སོགས་དྲ་ཐོག་གི་གནས་ཚུལ་རྣམས་ལྟ་མི་ཐུབ།',
offline_reconnect_prompt: 'དྲ་རྒྱ་ལ་སླར་སྦྲེལ་མཐུད་གནང་རྗེས་བསྐྱར་དུ་འབད་བརྩོན་བྱོས།',
dismiss: 'སྣང་མེད་གཏོང་བ།',
}
};

Expand Down Expand Up @@ -666,7 +694,7 @@ export function LanguageProvider({ children }: { children: React.ReactNode }) {

const t = (key: string, replacements?: Record<string, string | number>): string => {
const dict = translations[language];
let value = (dict as any)[key] || (translations['en'] as any)[key] || key;
let value = (dict as Record<string, string>)[key] || (translations['en'] as Record<string, string>)[key] || key;

if (replacements) {
Object.entries(replacements).forEach(([k, val]) => {
Expand Down
70 changes: 70 additions & 0 deletions frontend/components/pwa/network-status-indicator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
'use client';

import React, { useEffect, useState, useRef, useSyncExternalStore } from 'react';
import { WifiOff, Wifi } from 'lucide-react';
import { useLanguage } from '@/components/providers/language-provider';

function subscribeToOnlineStatus(callback: () => void) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}

function getOnlineSnapshot() {
return navigator.onLine;
}

function getServerSnapshot() {
return true;
}

export function NetworkStatusIndicator() {
const { t } = useLanguage();
const isOnline = useSyncExternalStore(
subscribeToOnlineStatus,
getOnlineSnapshot,
getServerSnapshot
);
const [showRestoredBanner, setShowRestoredBanner] = useState<boolean>(false);
const prevOnlineRef = useRef<boolean>(true);

useEffect(() => {
if (!prevOnlineRef.current && isOnline) {
// Transitioned from offline to online
setShowRestoredBanner(true);
const timer = setTimeout(() => {
setShowRestoredBanner(false);
}, 3500);
return () => clearTimeout(timer);
}
prevOnlineRef.current = isOnline;
}, [isOnline]);

// Normal online state: render nothing
if (isOnline && !showRestoredBanner) {
return null;
}

return (
<div
role="status"
aria-live="polite"
className="fixed top-22 left-1/2 -translate-x-1/2 z-50 pointer-events-none transition-all duration-300"
>
{!isOnline ? (
<div className="flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-amber-500/90 text-white dark:bg-amber-600/90 shadow-md backdrop-blur-sm text-xs sm:text-sm font-medium animate-in fade-in slide-in-from-top-2 duration-200">
<WifiOff className="w-3.5 h-3.5 shrink-0" />
<span>{t('you_are_offline')}</span>
</div>
) : showRestoredBanner ? (
<div className="flex items-center gap-2 px-3.5 py-1.5 rounded-full bg-emerald-600/90 text-white dark:bg-emerald-500/90 shadow-md backdrop-blur-sm text-xs sm:text-sm font-medium animate-in fade-in slide-in-from-top-2 duration-200">
<Wifi className="w-3.5 h-3.5 shrink-0" />
<span>{t('back_online')}</span>
</div>
) : null}
</div>
);
}
Loading
Loading