feat: add sharing feature
This commit is contained in:
+28
@@ -80,6 +80,7 @@
|
|||||||
<div class="user-details">
|
<div class="user-details">
|
||||||
<span id="userName">Welcome, User!</span>
|
<span id="userName">Welcome, User!</span>
|
||||||
<div class="header-buttons">
|
<div class="header-buttons">
|
||||||
|
<button id="shareBtn" class="header-btn">🔗 Share</button>
|
||||||
<button id="calendarBtn" class="header-btn">📅 Calendar</button>
|
<button id="calendarBtn" class="header-btn">📅 Calendar</button>
|
||||||
<button id="settingsBtn" class="header-btn">⚙️ Settings</button>
|
<button id="settingsBtn" class="header-btn">⚙️ Settings</button>
|
||||||
<button id="logoutBtn" class="header-btn logout">🚪 Logout</button>
|
<button id="logoutBtn" class="header-btn logout">🚪 Logout</button>
|
||||||
@@ -89,6 +90,7 @@
|
|||||||
<button id="mobileMenuToggle" class="mobile-menu-toggle hidden">☰</button>
|
<button id="mobileMenuToggle" class="mobile-menu-toggle hidden">☰</button>
|
||||||
<!-- Mobile dropdown menu -->
|
<!-- Mobile dropdown menu -->
|
||||||
<div id="mobileMenu" class="mobile-menu hidden">
|
<div id="mobileMenu" class="mobile-menu hidden">
|
||||||
|
<button id="shareBtnMobile" class="mobile-menu-item">🔗 Share</button>
|
||||||
<button id="calendarBtnMobile" class="mobile-menu-item">📅 Calendar</button>
|
<button id="calendarBtnMobile" class="mobile-menu-item">📅 Calendar</button>
|
||||||
<button id="settingsBtnMobile" class="mobile-menu-item">⚙️ Settings</button>
|
<button id="settingsBtnMobile" class="mobile-menu-item">⚙️ Settings</button>
|
||||||
<button id="logoutBtnMobile" class="mobile-menu-item logout">🚪 Logout</button>
|
<button id="logoutBtnMobile" class="mobile-menu-item logout">🚪 Logout</button>
|
||||||
@@ -103,6 +105,32 @@
|
|||||||
<p>Loading...</p>
|
<p>Loading...</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Share Modal -->
|
||||||
|
<div id="share-modal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-content">
|
||||||
|
<h2>🔗 Share This Day</h2>
|
||||||
|
<p>Share your food log for this day with others!</p>
|
||||||
|
<div class="share-link-container">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="share-link-input"
|
||||||
|
class="share-link-input"
|
||||||
|
readonly
|
||||||
|
placeholder="Generating share link..."
|
||||||
|
>
|
||||||
|
<button id="copy-share-link" class="copy-btn">Copy</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-buttons">
|
||||||
|
<button id="close-share-modal" class="modal-btn">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Shared View Banner -->
|
||||||
|
<div id="shared-view-banner" class="shared-view-banner hidden">
|
||||||
|
<span id="shared-view-text">👁️ Viewing shared day from <strong id="shared-user-name"></strong> on <strong id="shared-date"></strong></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Main App Content -->
|
<!-- Main App Content -->
|
||||||
<main id="app-content" class="hidden">
|
<main id="app-content" class="hidden">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
|
|||||||
+55
-2
@@ -1,5 +1,5 @@
|
|||||||
import { Client, Account, Databases, Query } from 'appwrite';
|
import { Client, Account, Databases, Query, ID } from 'appwrite';
|
||||||
import { DailyTotalCalories, FoodStorage, UserSettings } from './types';
|
import { DailyTotalCalories, FoodStorage, UserSettings, SharedDay } from './types';
|
||||||
|
|
||||||
// Initialize Appwrite client
|
// Initialize Appwrite client
|
||||||
const client = new 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 FOOD_ENTRIES_COLLECTION_ID = import.meta.env.VITE_APPWRITE_FOODENTRIESID
|
||||||
export const USER_SETTINGS_COLLECTION_ID = import.meta.env.VITE_APPWRITE_USERSETTINGSID
|
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 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
|
// Auth helper functions
|
||||||
export class AppwriteAuth {
|
export class AppwriteAuth {
|
||||||
@@ -277,4 +278,56 @@ export class AppwriteDB {
|
|||||||
throw error;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+218
@@ -21,6 +21,15 @@ document.addEventListener('DOMContentLoaded', async function() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
async function initializeAuth() {
|
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();
|
showLoading();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -404,6 +413,12 @@ const setupEventListeners = () => {
|
|||||||
// Settings form
|
// Settings form
|
||||||
document.getElementById('settingsForm')?.addEventListener('submit', handleSaveSettings);
|
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
|
// Search functionality
|
||||||
getInputById('foodSearchInput').addEventListener('input', function(e: Event) {
|
getInputById('foodSearchInput').addEventListener('input', function(e: Event) {
|
||||||
const target = e.target as HTMLInputElement;
|
const target = e.target as HTMLInputElement;
|
||||||
@@ -1300,4 +1315,207 @@ function createDayElement(day: number, isOtherMonth: boolean, year: number, mont
|
|||||||
return dayElement;
|
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;
|
(window as any).selectFood = selectFood;
|
||||||
|
|||||||
+6
-2
@@ -1,15 +1,19 @@
|
|||||||
import { DailyTotalCalories, FoodItem } from "./types";
|
import { DailyTotalCalories, FoodItem, SharedDay } from "./types";
|
||||||
|
|
||||||
interface AppState {
|
interface AppState {
|
||||||
currentHighlightIndex: number;
|
currentHighlightIndex: number;
|
||||||
searchTimeout: number | null;
|
searchTimeout: number | null;
|
||||||
searchResults: FoodItem[];
|
searchResults: FoodItem[];
|
||||||
calendarMonthlyCalories: DailyTotalCalories[];
|
calendarMonthlyCalories: DailyTotalCalories[];
|
||||||
|
isSharedView: boolean;
|
||||||
|
sharedData: SharedDay | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appState: AppState = {
|
export const appState: AppState = {
|
||||||
currentHighlightIndex: -1,
|
currentHighlightIndex: -1,
|
||||||
searchTimeout: null,
|
searchTimeout: null,
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
calendarMonthlyCalories: []
|
calendarMonthlyCalories: [],
|
||||||
|
isSharedView: false,
|
||||||
|
sharedData: null
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -33,3 +33,13 @@ export type DailyTotalCalories = {
|
|||||||
day: number;
|
day: number;
|
||||||
totalCalories: number;
|
totalCalories: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SharedDay = {
|
||||||
|
id?: string;
|
||||||
|
shareId: string;
|
||||||
|
userId: string;
|
||||||
|
userName: string;
|
||||||
|
date: string;
|
||||||
|
foodEntries: string; // JSON stringified FoodStorage[]
|
||||||
|
createdAt: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -774,3 +774,130 @@ label {
|
|||||||
padding: 16px;
|
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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user