feat: add sharing feature

This commit is contained in:
2025-10-30 13:18:01 -03:00
parent 9343efc377
commit 35bfec9915
6 changed files with 446 additions and 6 deletions
+55 -2
View File
@@ -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;
}
}
}
+220 -2
View File
@@ -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;
+6 -2
View File
@@ -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
};
+10
View File
@@ -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;
};