feat: add sharing feature
This commit is contained in:
+55
-2
@@ -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
@@ -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
@@ -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
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user