diff --git a/src/appwrite.ts b/src/appwrite.ts
index fa9534a..a7fa75c 100644
--- a/src/appwrite.ts
+++ b/src/appwrite.ts
@@ -1,5 +1,5 @@
-import { Client, Account, Databases, Query } from 'appwrite';
-import { DailyTotalCalories, FoodStorage, UserSettings } from './types';
+import { Client, Account, Databases, Query, ID } from 'appwrite';
+import { DailyTotalCalories, FoodStorage, UserSettings, SharedDay } from './types';
// Initialize Appwrite client
const client = new Client();
@@ -17,6 +17,7 @@ export const DATABASE_ID = import.meta.env.VITE_APPWRITE_DBID
export const FOOD_ENTRIES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_FOODENTRIESID
export const USER_SETTINGS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_USERSETTINGSID
export const MONTHLY_CALORIES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_MONTLYCALORIESID
+export const SHARED_DAYS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_SHAREDDAYSID
// Auth helper functions
export class AppwriteAuth {
@@ -277,4 +278,56 @@ export class AppwriteDB {
throw error;
}
}
+
+ // Create shared day snapshot
+ static async createSharedDay(date: Date, foodEntries: FoodStorage[]) {
+ try {
+ const user = await account.get();
+ const shareId = ID.unique();
+ const localDateTime = new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString();
+
+ const response = await databases.createDocument(
+ DATABASE_ID,
+ SHARED_DAYS_COLLECTION_ID,
+ 'unique()',
+ {
+ shareId: shareId,
+ userId: user.$id,
+ userName: user.name,
+ date: localDateTime.split('T')[0],
+ foodEntries: JSON.stringify(foodEntries),
+ createdAt: new Date().toISOString()
+ }
+ );
+ console.debug('Shared day created:', response);
+ return response;
+ } catch (error) {
+ console.error('Create shared day error:', error);
+ throw error;
+ }
+ }
+
+ // Get shared day by share ID
+ static async getSharedDay(shareId: string) {
+ try {
+ const response = await databases.listDocuments(
+ DATABASE_ID,
+ SHARED_DAYS_COLLECTION_ID,
+ [
+ Query.equal('shareId', shareId),
+ Query.limit(1)
+ ]
+ );
+
+ if (response.documents.length === 0) {
+ throw new Error('Shared day not found');
+ }
+
+ console.debug('Shared day retrieved:', response.documents[0]);
+ return response.documents[0];
+ } catch (error) {
+ console.error('Get shared day error:', error);
+ throw error;
+ }
+ }
}
diff --git a/src/index.ts b/src/index.ts
index b3191fe..fd93c68 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -21,11 +21,20 @@ document.addEventListener('DOMContentLoaded', async function() {
});
async function initializeAuth() {
+ // Check if this is a shared view first
+ const urlParams = new URLSearchParams(window.location.search);
+ const shareId = urlParams.get('share');
+
+ if (shareId) {
+ await checkSharedView();
+ return;
+ }
+
showLoading();
-
+
try {
const isLoggedIn = await AppwriteAuth.isLoggedIn();
-
+
if (isLoggedIn) {
currentUser = await AppwriteAuth.getCurrentUser();
await showMainApp();
@@ -404,6 +413,12 @@ const setupEventListeners = () => {
// Settings form
document.getElementById('settingsForm')?.addEventListener('submit', handleSaveSettings);
+ // Share button event listeners
+ document.getElementById('shareBtn')?.addEventListener('click', handleShareClick);
+ document.getElementById('shareBtnMobile')?.addEventListener('click', handleShareClick);
+ document.getElementById('close-share-modal')?.addEventListener('click', closeShareModal);
+ document.getElementById('copy-share-link')?.addEventListener('click', copyShareLink);
+
// Search functionality
getInputById('foodSearchInput').addEventListener('input', function(e: Event) {
const target = e.target as HTMLInputElement;
@@ -1300,4 +1315,207 @@ function createDayElement(day: number, isOtherMonth: boolean, year: number, mont
return dayElement;
}
+// Share functionality
+async function handleShareClick() {
+ if (!currentUser) {
+ swal('Hey!', 'Please log in to share your food log.', 'info');
+ return;
+ }
+
+ if (appState.isSharedView) {
+ swal('Info', 'You cannot share a shared view.', 'info');
+ return;
+ }
+
+ closeMobileMenu();
+ showLoading();
+
+ try {
+ // Get current day's food entries
+ const entries = await AppwriteDB.getFoodEntries(selectedDate);
+
+ if (entries.length === 0) {
+ hideLoading();
+ swal('Info', 'No food entries to share for this day.', 'info');
+ return;
+ }
+
+ // Convert entries to FoodStorage format
+ const foodEntries: FoodStorage[] = entries.map(entry => ({
+ name: entry.name,
+ grams: entry.grams,
+ calories: entry.calories,
+ protein: entry.protein,
+ fat: entry.fat,
+ carbs: entry.carbs,
+ fiber: entry.fiber,
+ time: entry.time,
+ date: entry.date,
+ alkaline: entry.alkaline,
+ }));
+
+ // Create shared day snapshot
+ const sharedDay = await AppwriteDB.createSharedDay(selectedDate, foodEntries);
+
+ // Generate share URL
+ const shareUrl = `${window.location.origin}${window.location.pathname}?share=${sharedDay.shareId}`;
+
+ // Show modal with link
+ getInputById('share-link-input').value = shareUrl;
+ getDivById('share-modal').classList.remove('hidden');
+
+ hideLoading();
+ } catch (error) {
+ hideLoading();
+ console.error('Share error:', error);
+ if (error instanceof Error) {
+ swal('Oh no!', 'Failed to create share link: ' + error.message, 'error');
+ }
+ }
+}
+
+function closeShareModal() {
+ getDivById('share-modal').classList.add('hidden');
+}
+
+async function copyShareLink() {
+ const shareInput = getInputById('share-link-input');
+
+ try {
+ await navigator.clipboard.writeText(shareInput.value);
+ const copyBtn = document.getElementById('copy-share-link');
+ if (copyBtn) {
+ const originalText = copyBtn.textContent;
+ copyBtn.textContent = 'Copied!';
+ setTimeout(() => {
+ copyBtn.textContent = originalText;
+ }, 2000);
+ }
+ } catch (error) {
+ console.error('Copy error:', error);
+ swal('Oh no!', 'Failed to copy link to clipboard', 'error');
+ }
+}
+
+// Check for share parameter on page load
+async function checkSharedView() {
+ const urlParams = new URLSearchParams(window.location.search);
+ const shareId = urlParams.get('share');
+
+ if (shareId) {
+ showLoading();
+
+ try {
+ const sharedDay = await AppwriteDB.getSharedDay(shareId);
+
+ // Set shared view mode
+ appState.isSharedView = true;
+ appState.sharedData = {
+ shareId: sharedDay.shareId,
+ userId: sharedDay.userId,
+ userName: sharedDay.userName,
+ date: sharedDay.date,
+ foodEntries: sharedDay.foodEntries,
+ createdAt: sharedDay.createdAt
+ };
+
+ // Parse the date and set as selected date (as local date, not UTC)
+ const [year, month, day] = sharedDay.date.split('-').map(Number);
+ selectedDate = new Date(year, month - 1, day);
+ currentViewDate = new Date(year, month - 1, day);
+
+ // Show shared view banner
+ const banner = getDivById('shared-view-banner');
+ banner.classList.remove('hidden');
+ getDivById('shared-user-name').textContent = sharedDay.userName;
+ getDivById('shared-date').textContent = selectedDate.toLocaleDateString('en-US', {
+ weekday: 'long',
+ year: 'numeric',
+ month: 'long',
+ day: 'numeric'
+ });
+
+ // Hide auth section and show app content
+ getDivById('auth-section').classList.add('hidden');
+ getDivById('user-info').classList.add('hidden');
+ getDivById('app-content').classList.remove('hidden');
+
+ // Render shared day view
+ renderSharedDayView();
+
+ // Apply read-only mode
+ applyReadOnlyMode();
+
+ hideLoading();
+ } catch (error) {
+ hideLoading();
+ console.error('Load shared day error:', error);
+ swal('Oh no!', 'Failed to load shared day: ' + (error instanceof Error ? error.message : 'Unknown error'), 'error');
+ }
+ }
+}
+
+function renderSharedDayView() {
+ if (!appState.sharedData) return;
+
+ updateCurrentDate();
+
+ // Parse food entries
+ const foodEntries: FoodStorage[] = JSON.parse(appState.sharedData.foodEntries);
+
+ // Clear container
+ getDivById('foodCardsContainer').innerHTML = '';
+
+ // Add each entry to view
+ foodEntries.forEach((entry, index) => {
+ addFoodToView(entry, `shared-${index}`);
+ });
+
+ // Calculate and display totals
+ let totalCalories = 0;
+ let totalProtein = 0;
+ let totalFat = 0;
+ let totalCarbs = 0;
+ let totalFiber = 0;
+
+ foodEntries.forEach(entry => {
+ totalCalories += entry.calories;
+ totalProtein += entry.protein;
+ totalFat += entry.fat;
+ totalCarbs += entry.carbs;
+ totalFiber += entry.fiber;
+ });
+
+ getDivById('caloriesCounter').textContent = totalCalories.toString();
+ getDivById('proteinValue').textContent = (Math.round(totalProtein * 10) / 10).toString();
+ getDivById('fatValue').textContent = (Math.round(totalFat * 10) / 10).toString();
+ getDivById('carboValue').textContent = (Math.round(totalCarbs * 10) / 10).toString();
+ getDivById('fiberValue').textContent = (Math.round(totalFiber * 10) / 10).toString();
+
+ // Update alkaline level
+ updateTotalCalories();
+
+ renderCalendar();
+}
+
+function applyReadOnlyMode() {
+ // Hide add food section
+ const addFoodSection = document.querySelector('.add-food-section');
+ if (addFoodSection) {
+ addFoodSection.classList.add('read-only-disabled');
+ }
+
+ // Hide edit/delete/copy buttons
+ const actionButtons = document.querySelectorAll('.card-actions button');
+ actionButtons.forEach(button => {
+ (button as HTMLElement).style.display = 'none';
+ });
+
+ // Hide calendar navigation
+ const calendarSection = document.querySelector('.calendar-section');
+ if (calendarSection) {
+ calendarSection.classList.add('read-only-disabled');
+ }
+}
+
(window as any).selectFood = selectFood;
diff --git a/src/state.ts b/src/state.ts
index 4161ba2..10c422d 100644
--- a/src/state.ts
+++ b/src/state.ts
@@ -1,15 +1,19 @@
-import { DailyTotalCalories, FoodItem } from "./types";
+import { DailyTotalCalories, FoodItem, SharedDay } from "./types";
interface AppState {
currentHighlightIndex: number;
searchTimeout: number | null;
searchResults: FoodItem[];
calendarMonthlyCalories: DailyTotalCalories[];
+ isSharedView: boolean;
+ sharedData: SharedDay | null;
}
export const appState: AppState = {
currentHighlightIndex: -1,
searchTimeout: null,
searchResults: [],
- calendarMonthlyCalories: []
+ calendarMonthlyCalories: [],
+ isSharedView: false,
+ sharedData: null
};
diff --git a/src/types.ts b/src/types.ts
index d4e197b..305dcf8 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -33,3 +33,13 @@ export type DailyTotalCalories = {
day: number;
totalCalories: number;
};
+
+export type SharedDay = {
+ id?: string;
+ shareId: string;
+ userId: string;
+ userName: string;
+ date: string;
+ foodEntries: string; // JSON stringified FoodStorage[]
+ createdAt: string;
+};
diff --git a/style.css b/style.css
index b5146c3..e06dd5d 100644
--- a/style.css
+++ b/style.css
@@ -774,3 +774,130 @@ label {
padding: 16px;
}
}
+
+/* Share Modal Styles */
+.modal-overlay {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.8);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 2000;
+ backdrop-filter: blur(5px);
+}
+
+.modal-overlay.hidden {
+ display: none;
+}
+
+.modal-content {
+ background: #2a2a2a;
+ border-radius: 12px;
+ padding: 30px;
+ max-width: 500px;
+ width: 90%;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
+ color: #e0e0e0;
+}
+
+.modal-content h2 {
+ margin-bottom: 15px;
+ color: #b0b0b0;
+ font-size: 1.5rem;
+}
+
+.modal-content p {
+ margin-bottom: 20px;
+ color: #999;
+}
+
+.share-link-container {
+ display: flex;
+ gap: 10px;
+ margin-bottom: 20px;
+}
+
+.share-link-input {
+ flex: 1;
+ padding: 12px 15px;
+ background: #1a1a1a;
+ border: 2px solid #404040;
+ border-radius: 6px;
+ color: #e0e0e0;
+ font-family: monospace;
+ font-size: 14px;
+ outline: none;
+}
+
+.copy-btn {
+ padding: 12px 24px;
+ background: linear-gradient(135deg, #4CAF50, #45a049);
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ font-weight: 600;
+ transition: all 0.3s ease;
+}
+
+.copy-btn:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 5px 15px rgba(76, 175, 80, 0.3);
+}
+
+.copy-btn:active {
+ transform: translateY(0);
+}
+
+.modal-buttons {
+ display: flex;
+ justify-content: flex-end;
+ gap: 10px;
+}
+
+.modal-btn {
+ padding: 10px 20px;
+ background: #404040;
+ color: white;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.modal-btn:hover {
+ background: #505050;
+}
+
+/* Shared View Banner */
+.shared-view-banner {
+ background: linear-gradient(135deg, #FF6B6B, #FF8E53);
+ color: white;
+ padding: 15px 20px;
+ text-align: center;
+ font-size: 16px;
+ font-weight: 500;
+ box-shadow: 0 2px 10px rgba(255, 107, 107, 0.3);
+ position: sticky;
+ top: 0;
+ z-index: 1000;
+}
+
+.shared-view-banner.hidden {
+ display: none;
+}
+
+.shared-view-banner strong {
+ font-weight: 700;
+}
+
+/* Disabled state for read-only mode */
+.read-only-disabled {
+ opacity: 0.5;
+ pointer-events: none;
+ cursor: not-allowed !important;
+}