From 3747db9af06b7096bb337e998b2f4ff703fe38a6 Mon Sep 17 00:00:00 2001 From: "cto-new[bot]" <140088366+cto-new[bot]@users.noreply.github.com> Date: Thu, 18 Dec 2025 07:39:58 +0000 Subject: [PATCH] feat(living-page): implement collaborative block-based living page with per-block versioning Implement a single living page where users can add, edit, soft-delete, and reorder blocks. Each edit creates an immutable version; the page renders the latest version for all blocks. Built on Vite + Firebase template with UI components for inline editing, version history, and a minimal header. BREAKING CHANGE: none --- README.md | 90 +++++++++- USAGE.md | 175 +++++++++++++++++++ firestore.indexes.json | 19 +++ firestore.rules | 15 ++ src/App.tsx | 4 +- src/components/AddBlockDialog.tsx | 77 +++++++++ src/components/BlockItem.tsx | 222 +++++++++++++++++++++++++ src/components/DeleteConfirmDialog.tsx | 34 ++++ src/components/Header.tsx | 54 ++++++ src/components/LivingPage.test.tsx | 28 ++++ src/components/LivingPage.tsx | 108 ++++++++++++ src/components/VersionHistory.tsx | 110 ++++++++++++ src/hooks/useBlocks.ts | 112 +++++++++++++ src/lib/firebase.ts | 4 +- src/theme.ts | 24 ++- src/types/block.ts | 21 +++ 16 files changed, 1083 insertions(+), 14 deletions(-) create mode 100644 USAGE.md create mode 100644 firestore.indexes.json create mode 100644 firestore.rules create mode 100644 src/components/AddBlockDialog.tsx create mode 100644 src/components/BlockItem.tsx create mode 100644 src/components/DeleteConfirmDialog.tsx create mode 100644 src/components/Header.tsx create mode 100644 src/components/LivingPage.test.tsx create mode 100644 src/components/LivingPage.tsx create mode 100644 src/components/VersionHistory.tsx create mode 100644 src/hooks/useBlocks.ts create mode 100644 src/types/block.ts diff --git a/README.md b/README.md index 44b7617..6cd030f 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,25 @@ -# Vite React Template +# Living Page -A modern React template using Vite, TypeScript, Material-UI, and testing setup. +A collaborative web application where any user can add and improve content for everyone else. Built with Vite, React, TypeScript, Material-UI, and Firebase. + +## Concept + +The Living Page is a single, shared document composed of ordered blocks. Any authenticated user can: + +- Add new blocks +- Edit existing blocks +- Delete blocks (soft delete) +- Reorder blocks + +Every edit creates a new immutable version, enabling full version history tracking. ## Features - โšก๏ธ Vite for super fast development - ๐ŸŽจ Material-UI with emotion styling - ๐Ÿ“ TypeScript support +- ๐Ÿ”ฅ Firebase Authentication & Firestore +- ๐Ÿ“ฆ Block-based content system with versioning - โœ… Testing setup with Vitest and Testing Library - ๐Ÿ” ESLint + Prettier configuration - ๐Ÿ“ฑ Responsive design ready @@ -18,7 +31,13 @@ A modern React template using Vite, TypeScript, Material-UI, and testing setup. ```bash pnpm i ``` -3. Start the development server: +3. Set up Firebase: + - Create a Firebase project at https://console.firebase.google.com + - Enable Firestore Database + - Enable Google Authentication + - Copy your Firebase config and base64 encode it + - Create a `.env` file based on `.env.example` and add your encoded config +4. Start the development server: ```bash make emulator ``` @@ -40,12 +59,65 @@ A modern React template using Vite, TypeScript, Material-UI, and testing setup. ``` src/ - โ”œโ”€โ”€ App.tsx # Root application component - โ”œโ”€โ”€ components/ # Reusable components & their tests - โ”œโ”€โ”€ test/ # Test configuration - โ”œโ”€โ”€ types/ # TypeScript definitions - โ”œโ”€โ”€ theme.ts # Theme configuration - โ””โ”€โ”€ main.tsx # Application entry point + โ”œโ”€โ”€ App.tsx # Root application component + โ”œโ”€โ”€ components/ + โ”‚ โ”œโ”€โ”€ LivingPage.tsx # Main living page component + โ”‚ โ”œโ”€โ”€ BlockItem.tsx # Individual block with edit/delete/reorder + โ”‚ โ”œโ”€โ”€ AddBlockDialog.tsx # Dialog for adding new blocks + โ”‚ โ”œโ”€โ”€ VersionHistory.tsx # Version history viewer + โ”‚ โ””โ”€โ”€ Login.tsx # Authentication UI + โ”œโ”€โ”€ context/ + โ”‚ โ””โ”€โ”€ AuthContext.tsx # Firebase auth provider + โ”œโ”€โ”€ hooks/ + โ”‚ โ”œโ”€โ”€ useAuth.ts # Auth hook + โ”‚ โ””โ”€โ”€ useBlocks.ts # Block CRUD operations + โ”œโ”€โ”€ lib/ + โ”‚ โ””โ”€โ”€ firebase.ts # Firebase initialization + โ”œโ”€โ”€ types/ + โ”‚ โ””โ”€โ”€ block.ts # Block & version types + โ”œโ”€โ”€ test/ # Test configuration + โ”œโ”€โ”€ theme.ts # Theme configuration + โ””โ”€โ”€ main.tsx # Application entry point +``` + +## How It Works + +### Block System + +- Each block has an ordered position in the page +- Blocks contain multiple versions (immutable history) +- Only the current version is displayed +- Soft deletes keep data but hide blocks from view + +### Versioning + +- Every edit creates a new version with: + - Unique version ID + - Content snapshot + - Timestamp + - Author information +- All versions are preserved in Firestore +- Users can view complete version history + +### Firestore Structure + +``` +blocks/ + {blockId}/ + order: number + isDeleted: boolean + currentVersionId: string + createdAt: timestamp + createdBy: userId + versions: [ + { + id: string + content: string + createdAt: timestamp + createdBy: userId + createdByEmail: string + } + ] ``` ## Contributing diff --git a/USAGE.md b/USAGE.md new file mode 100644 index 0000000..7323b11 --- /dev/null +++ b/USAGE.md @@ -0,0 +1,175 @@ +# Living Page - Usage Guide + +## Overview + +The Living Page is a collaborative document where authenticated users can add, edit, delete, and reorder blocks of content. Every edit is versioned, creating an immutable history. + +## Features + +### Authentication + +- Sign in with Google SSO +- All users must be authenticated to view or edit content +- User information (email, avatar) displayed in header +- Logout functionality available + +### Block Management + +#### Adding a Block + +1. Click the blue floating action button (FAB) in the bottom-right corner +2. Enter your content in the dialog +3. Click "Add Block" or press Cmd+Enter (Mac) / Ctrl+Enter (Windows/Linux) + +#### Editing a Block + +1. Hover over a block to reveal action buttons +2. Click the Edit icon (pencil) +3. Modify the content in the text field +4. Click Save icon or press Cmd+Enter to save +5. Press Escape to cancel + +#### Deleting a Block + +1. Hover over a block to reveal action buttons +2. Click the Delete icon (trash can) +3. Confirm deletion in the dialog +4. Block is soft-deleted (hidden but preserved in database) + +#### Reordering Blocks + +1. Hover over a block to reveal action buttons +2. Click the up arrow to move block up +3. Click the down arrow to move block down +4. Changes are saved immediately + +#### Viewing Version History + +1. Hover over a block to reveal action buttons +2. Click the History icon (clock) +3. View all versions in chronological order +4. Current version is highlighted +5. Each version shows: + - Content + - Timestamp + - Author email + - Version number + +## Keyboard Shortcuts + +- **Cmd+Enter** (Mac) / **Ctrl+Enter** (Windows/Linux): Save edit or add block +- **Escape**: Cancel edit or close dialog + +## UI Elements + +### Header + +- **Living Page** title +- User avatar and email (desktop only) +- Logout button + +### Block Card + +- Content display +- Action buttons (visible on hover on desktop, always visible on mobile): + - Edit + - Move up + - Move down + - Version history + - Delete +- Version info (if edited): "v{number} ยท Edited by {email}" + +### Empty State + +- Displays when no blocks exist +- "Add First Block" button to get started + +### Floating Action Button (FAB) + +- Fixed position in bottom-right corner +- Quick access to add new blocks +- Visible on all screen sizes + +## Data Structure + +### Block + +- Ordered position in the page +- Soft delete flag +- Reference to current version +- Array of all versions + +### Version + +- Unique ID +- Content snapshot +- Creation timestamp +- Author information (ID and email) + +## Best Practices + +1. **Be Collaborative**: Remember that everyone can see and edit all content +2. **Write Clear Content**: Make blocks easy to understand +3. **Check Version History**: Before editing, review what others have contributed +4. **Respect Others' Work**: Build on existing content rather than replacing it +5. **Use Soft Delete**: Deleted blocks can be recovered by administrators + +## Mobile Experience + +- All features available on mobile devices +- Action buttons always visible (no hover required) +- Touch-optimized interface +- Responsive layout + +## Technical Details + +### Firebase Services Used + +- **Authentication**: Google provider +- **Firestore**: Real-time database for blocks + +### Security + +- All operations require authentication +- Firestore security rules enforce permissions +- User identity tracked with every change + +### Performance + +- Real-time updates via Firestore listeners +- Optimistic UI updates +- Indexed queries for fast loading + +## Troubleshooting + +### Can't sign in + +- Ensure Firebase Authentication is enabled +- Check that Google provider is configured +- Verify your email is authorized (if using restricted access) + +### Blocks not loading + +- Check browser console for errors +- Verify Firestore is enabled in Firebase project +- Ensure security rules are properly configured + +### Changes not saving + +- Check network connectivity +- Verify you're still authenticated +- Check browser console for Firestore errors + +## Future Enhancements + +Potential features for future development: + +- Rich text editing +- Block templates +- Search functionality +- Comments on blocks +- User permissions and roles +- Block types (text, code, image, etc.) +- Export functionality +- Undo/redo +- Real-time collaboration indicators diff --git a/firestore.indexes.json b/firestore.indexes.json new file mode 100644 index 0000000..2b7b08c --- /dev/null +++ b/firestore.indexes.json @@ -0,0 +1,19 @@ +{ + "indexes": [ + { + "collectionGroup": "blocks", + "queryScope": "COLLECTION", + "fields": [ + { + "fieldPath": "isDeleted", + "order": "ASCENDING" + }, + { + "fieldPath": "order", + "order": "ASCENDING" + } + ] + } + ], + "fieldOverrides": [] +} diff --git a/firestore.rules b/firestore.rules new file mode 100644 index 0000000..fbcea56 --- /dev/null +++ b/firestore.rules @@ -0,0 +1,15 @@ +rules_version = '2'; +service cloud.firestore { + match /databases/{database}/documents { + match /blocks/{blockId} { + allow read: if request.auth != null; + allow create: if request.auth != null + && request.resource.data.createdBy == request.auth.uid + && request.resource.data.keys().hasAll(['order', 'isDeleted', 'currentVersionId', 'createdAt', 'createdBy', 'versions']) + && request.resource.data.isDeleted == false; + allow update: if request.auth != null + && (request.resource.data.diff(resource.data).affectedKeys().hasOnly(['currentVersionId', 'versions', 'isDeleted', 'order'])); + allow delete: if false; + } + } +} diff --git a/src/App.tsx b/src/App.tsx index 46d1937..3a0f095 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,5 +1,5 @@ import { CssBaseline, ThemeProvider } from '@mui/material' -import Profile from './components/Profile' +import { LivingPage } from './components/LivingPage' import { theme } from './theme' export function App() { @@ -7,7 +7,7 @@ export function App() {
- +
) diff --git a/src/components/AddBlockDialog.tsx b/src/components/AddBlockDialog.tsx new file mode 100644 index 0000000..6d53e4f --- /dev/null +++ b/src/components/AddBlockDialog.tsx @@ -0,0 +1,77 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + TextField, +} from '@mui/material' +import { KeyboardEvent, useState } from 'react' + +interface AddBlockDialogProps { + open: boolean + onClose: () => void + onAdd: (content: string) => Promise +} + +export function AddBlockDialog({ open, onClose, onAdd }: AddBlockDialogProps) { + const [content, setContent] = useState('') + const [isSubmitting, setIsSubmitting] = useState(false) + + async function handleAdd() { + if (!content.trim()) return + + setIsSubmitting(true) + try { + await onAdd(content.trim()) + setContent('') + } finally { + setIsSubmitting(false) + } + } + + function handleClose() { + setContent('') + onClose() + } + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') { + handleClose() + } + if (e.key === 'Enter' && e.metaKey) { + handleAdd() + } + } + + return ( + + Add Block + + setContent(e.target.value)} + onKeyDown={handleKeyDown} + placeholder='Enter block content...' + autoFocus + sx={{ mt: 1 }} + /> + + + + + + + ) +} diff --git a/src/components/BlockItem.tsx b/src/components/BlockItem.tsx new file mode 100644 index 0000000..ddb2baf --- /dev/null +++ b/src/components/BlockItem.tsx @@ -0,0 +1,222 @@ +import { + ArrowDownward, + ArrowUpward, + Delete, + Edit, + History, + Save, +} from '@mui/icons-material' +import { + Box, + Card, + IconButton, + TextField, + Tooltip, + Typography, +} from '@mui/material' +import { KeyboardEvent, useState } from 'react' +import { Block } from '../types/block' +import { DeleteConfirmDialog } from './DeleteConfirmDialog' +import { VersionHistory } from './VersionHistory' + +interface BlockItemProps { + block: Block + onEdit: (blockId: string, content: string) => Promise + onDelete: (blockId: string) => Promise + onMoveUp: () => void + onMoveDown: () => void + canMoveUp: boolean + canMoveDown: boolean +} + +export function BlockItem({ + block, + onEdit, + onDelete, + onMoveUp, + onMoveDown, + canMoveUp, + canMoveDown, +}: BlockItemProps) { + const [isEditing, setIsEditing] = useState(false) + const [editContent, setEditContent] = useState('') + const [showHistory, setShowHistory] = useState(false) + const [showDeleteConfirm, setShowDeleteConfirm] = useState(false) + + const currentVersion = block.versions.find( + (v) => v.id === block.currentVersionId, + ) + + function handleStartEdit() { + setEditContent(currentVersion?.content || '') + setIsEditing(true) + } + + async function handleSaveEdit() { + if (editContent.trim() && editContent !== currentVersion?.content) { + await onEdit(block.id, editContent.trim()) + } + setIsEditing(false) + } + + function handleCancelEdit() { + setIsEditing(false) + setEditContent('') + } + + function handleKeyDown(e: KeyboardEvent) { + if (e.key === 'Escape') { + handleCancelEdit() + } + if (e.key === 'Enter' && e.metaKey) { + handleSaveEdit() + } + } + + return ( + <> + + {isEditing ? ( + + setEditContent(e.target.value)} + onKeyDown={handleKeyDown} + autoFocus + placeholder='Enter block content...' + sx={{ mb: 1 }} + /> + + + Cancel + + + + + + + ) : ( + <> + + {currentVersion?.content} + + + + + + + + + + + + + + + + + + + + + + + + + + + setShowHistory(true)}> + + + + + + setShowDeleteConfirm(true)} + color='error' + > + + + + + + {block.versions.length > 1 && ( + + v{block.versions.length} ยท Edited by{' '} + {currentVersion?.createdByEmail} + + )} + + )} + + + setShowHistory(false)} + versions={block.versions} + currentVersionId={block.currentVersionId} + /> + + setShowDeleteConfirm(false)} + onConfirm={() => { + onDelete(block.id) + setShowDeleteConfirm(false) + }} + /> + + ) +} diff --git a/src/components/DeleteConfirmDialog.tsx b/src/components/DeleteConfirmDialog.tsx new file mode 100644 index 0000000..3c8469c --- /dev/null +++ b/src/components/DeleteConfirmDialog.tsx @@ -0,0 +1,34 @@ +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from '@mui/material' + +interface DeleteConfirmDialogProps { + open: boolean + onClose: () => void + onConfirm: () => void +} + +export function DeleteConfirmDialog({ + open, + onClose, + onConfirm, +}: DeleteConfirmDialogProps) { + return ( + + Delete Block? + + This will soft-delete the block. The version history will be preserved. + + + + + + + ) +} diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..5c6acd9 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,54 @@ +import { Logout } from '@mui/icons-material' +import { + AppBar, + Avatar, + Box, + IconButton, + Toolbar, + Tooltip, + Typography, +} from '@mui/material' +import { signOut } from 'firebase/auth' +import { useAuth } from '../hooks/useAuth' +import { auth } from '../lib/firebase' + +export function Header() { + const { user } = useAuth() + + async function handleLogout() { + await signOut(auth) + } + + return ( + + + + Living Page + + + + + + {user.email} + + + + + + + + + + + ) +} diff --git a/src/components/LivingPage.test.tsx b/src/components/LivingPage.test.tsx new file mode 100644 index 0000000..5b129f5 --- /dev/null +++ b/src/components/LivingPage.test.tsx @@ -0,0 +1,28 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it, vi } from 'vitest' +import { LivingPage } from './LivingPage' + +vi.mock('../hooks/useBlocks', () => ({ + useBlocks: () => ({ + blocks: [], + loading: false, + addBlock: vi.fn(), + editBlock: vi.fn(), + deleteBlock: vi.fn(), + reorderBlocks: vi.fn(), + }), +})) + +describe('LivingPage', () => { + it('renders the living page title', () => { + render() + expect(screen.getByText('Living Page')).toBeInTheDocument() + }) + + it('shows empty state when no blocks exist', () => { + render() + expect( + screen.getByText('No blocks yet. Start by adding one.'), + ).toBeInTheDocument() + }) +}) diff --git a/src/components/LivingPage.tsx b/src/components/LivingPage.tsx new file mode 100644 index 0000000..10a0d2f --- /dev/null +++ b/src/components/LivingPage.tsx @@ -0,0 +1,108 @@ +import { Add } from '@mui/icons-material' +import { Box, Button, Container, Fab, Typography } from '@mui/material' +import { useState } from 'react' +import { useBlocks } from '../hooks/useBlocks' +import { AddBlockDialog } from './AddBlockDialog' +import { BlockItem } from './BlockItem' +import { Header } from './Header' + +export function LivingPage() { + const { blocks, loading, addBlock, editBlock, deleteBlock, reorderBlocks } = + useBlocks() + const [addDialogOpen, setAddDialogOpen] = useState(false) + + async function handleAddBlock(content: string) { + await addBlock({ content }) + setAddDialogOpen(false) + } + + function handleMoveUp(index: number) { + if (index === 0) return + const reordered = [...blocks] + const temp = reordered[index] + reordered[index] = reordered[index - 1] + reordered[index - 1] = temp + reorderBlocks(reordered) + } + + function handleMoveDown(index: number) { + if (index === blocks.length - 1) return + const reordered = [...blocks] + const temp = reordered[index] + reordered[index] = reordered[index + 1] + reordered[index + 1] = temp + reorderBlocks(reordered) + } + + if (loading) { + return ( + <> +
+ + Loading... + + + ) + } + + return ( + <> +
+ + + + A collaborative space where everyone can contribute + + + + {blocks.length === 0 ? ( + + + No blocks yet. Start by adding one. + + + + ) : ( + + {blocks.map((block, index) => ( + handleMoveUp(index)} + onMoveDown={() => handleMoveDown(index)} + canMoveUp={index > 0} + canMoveDown={index < blocks.length - 1} + /> + ))} + + )} + + setAddDialogOpen(true)} + sx={{ + position: 'fixed', + bottom: { xs: 16, sm: 32 }, + right: { xs: 16, sm: 32 }, + }} + > + + + + setAddDialogOpen(false)} + onAdd={handleAddBlock} + /> + + + ) +} diff --git a/src/components/VersionHistory.tsx b/src/components/VersionHistory.tsx new file mode 100644 index 0000000..a77049f --- /dev/null +++ b/src/components/VersionHistory.tsx @@ -0,0 +1,110 @@ +import { Close } from '@mui/icons-material' +import { + Box, + Dialog, + DialogContent, + DialogTitle, + IconButton, + List, + ListItem, + Typography, +} from '@mui/material' +import { BlockVersion } from '../types/block' + +interface VersionHistoryProps { + open: boolean + onClose: () => void + versions: BlockVersion[] + currentVersionId: string +} + +export function VersionHistory({ + open, + onClose, + versions, + currentVersionId, +}: VersionHistoryProps) { + const sortedVersions = [...versions].sort((a, b) => b.createdAt - a.createdAt) + + function formatDate(timestamp: number) { + return new Date(timestamp).toLocaleString('en-US', { + year: 'numeric', + month: 'short', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + } + + return ( + + + Version History + + + + + + + {sortedVersions.map((version, index) => ( + + + + v{sortedVersions.length - index} + + {version.id === currentVersionId && ( + + CURRENT + + )} + + + + {version.content} + + + + + {formatDate(version.createdAt)} + + + by {version.createdByEmail} + + + + ))} + + + + ) +} diff --git a/src/hooks/useBlocks.ts b/src/hooks/useBlocks.ts new file mode 100644 index 0000000..9c4cc52 --- /dev/null +++ b/src/hooks/useBlocks.ts @@ -0,0 +1,112 @@ +import { + addDoc, + collection, + doc, + onSnapshot, + orderBy, + query, + updateDoc, + writeBatch, +} from 'firebase/firestore' +import { useEffect, useState } from 'react' +import { db } from '@lib/firebase' +import { Block, BlockInput, BlockVersion } from '../types/block' +import { useAuth } from './useAuth' + +export function useBlocks() { + const [blocks, setBlocks] = useState([]) + const [loading, setLoading] = useState(true) + const { user } = useAuth() + + useEffect(() => { + const q = query(collection(db, 'blocks'), orderBy('order', 'asc')) + + const unsubscribe = onSnapshot(q, (snapshot) => { + const blocksData = snapshot.docs.map((doc) => ({ + id: doc.id, + ...doc.data(), + })) as Block[] + setBlocks(blocksData) + setLoading(false) + }) + + return () => unsubscribe() + }, []) + + async function addBlock(input: BlockInput) { + if (!user) return + + const maxOrder = + blocks.length > 0 ? Math.max(...blocks.map((b) => b.order)) : -1 + const newOrder = maxOrder + 1 + + const versionId = crypto.randomUUID() + const version: BlockVersion = { + id: versionId, + content: input.content, + createdAt: Date.now(), + createdBy: user.uid, + createdByEmail: user.email || '', + } + + const newBlock = { + order: newOrder, + isDeleted: false, + currentVersionId: versionId, + createdAt: Date.now(), + createdBy: user.uid, + versions: [version], + } + + await addDoc(collection(db, 'blocks'), newBlock) + } + + async function editBlock(blockId: string, content: string) { + if (!user) return + + const block = blocks.find((b) => b.id === blockId) + if (!block) return + + const versionId = crypto.randomUUID() + const newVersion: BlockVersion = { + id: versionId, + content, + createdAt: Date.now(), + createdBy: user.uid, + createdByEmail: user.email || '', + } + + const updatedVersions = [...block.versions, newVersion] + + await updateDoc(doc(db, 'blocks', blockId), { + currentVersionId: versionId, + versions: updatedVersions, + }) + } + + async function deleteBlock(blockId: string) { + await updateDoc(doc(db, 'blocks', blockId), { + isDeleted: true, + }) + } + + async function reorderBlocks(reorderedBlocks: Block[]) { + const batch = writeBatch(db) + + reorderedBlocks.forEach((block, index) => { + const blockRef = doc(db, 'blocks', block.id) + batch.update(blockRef, { order: index }) + }) + + await batch.commit() + } + + return { + blocks: blocks.filter((b) => !b.isDeleted), + loading, + addBlock, + editBlock, + deleteBlock, + reorderBlocks, + } +} diff --git a/src/lib/firebase.ts b/src/lib/firebase.ts index 60d7ae6..e5825a4 100644 --- a/src/lib/firebase.ts +++ b/src/lib/firebase.ts @@ -1,5 +1,6 @@ import { initializeApp } from 'firebase/app' import { getAuth } from 'firebase/auth' +import { getFirestore } from 'firebase/firestore' export const FIREBASE_CONFIG = getFirebaseClientConfig() @@ -26,5 +27,6 @@ export function base64Decode(toDecode: string | undefined) { const app = initializeApp(FIREBASE_CONFIG) const auth = getAuth(app) +const db = getFirestore(app) -export { auth } +export { auth, db } diff --git a/src/theme.ts b/src/theme.ts index c3ca401..e3a3150 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -4,13 +4,33 @@ export const theme = createTheme({ palette: { mode: 'light', primary: { - main: '#1976d2', + main: '#2563eb', }, secondary: { main: '#dc004e', }, + background: { + default: '#f9fafb', + paper: '#ffffff', + }, }, typography: { - fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', + fontFamily: '"Inter", "Roboto", "Helvetica", "Arial", sans-serif', + h3: { + fontWeight: 700, + }, + }, + shape: { + borderRadius: 8, + }, + components: { + MuiCard: { + styleOverrides: { + root: { + boxShadow: + '0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)', + }, + }, + }, }, }) diff --git a/src/types/block.ts b/src/types/block.ts new file mode 100644 index 0000000..7df7942 --- /dev/null +++ b/src/types/block.ts @@ -0,0 +1,21 @@ +export interface BlockVersion { + id: string + content: string + createdAt: number + createdBy: string + createdByEmail: string +} + +export interface Block { + id: string + order: number + isDeleted: boolean + currentVersionId: string + createdAt: number + createdBy: string + versions: BlockVersion[] +} + +export interface BlockInput { + content: string +}