diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..60ff4ec --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,36 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev" + }, + { + "name": "Next.js: debug client-side", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000" + }, + { + "name": "Next.js: debug full stack", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/.bin/next", + "runtimeArgs": [ + "--inspect" + ], + "skipFiles": [ + "/**" + ], + "serverReadyAction": { + "action": "debugWithEdge", + "killOnServerStop": true, + "pattern": "- Local:.+(https?://.+)", + "uriFormat": "%s", + "webRoot": "${workspaceFolder}" + } + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 7dd7bd6..7b02772 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,13 +3,14 @@ "editor.formatOnSave": true, "files.exclude": { // "build":true, - "test": true, + // "test": true, // ".vscode":false, // ".*":true, - "web": false, - "build": false, - "windows": true, - "macos": true, + // "web": false, + // "build": false, + // "windows": true, + // "macos": true, + // ".next": true }, "dart.flutterSdkPath": "/Users/mahesh/Documents/flutter", "dart.debugExternalLibraries": true, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 681ced6..5829d80 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -1,34 +1,10 @@ { - "version": "2.0.0", - "tasks": [ - { - "type": "flutter", - "command": "flutter", - "args": [ - "pub", - "get" - ], - "problemMatcher": [], - "label": "flutter: flutter pub get", - "detail": "", - "runOptions": { - "runOn": "folderOpen" - } - }, - // { - // "type": "flutter", - // "command": "flutter", - // "args": [ - // "build", - // "web" - // ], - // "group": "build", - // "problemMatcher": [], - // "label": "flutter: flutter build web", - // "detail": "", - // "runOptions": { - // "runOn": "folderOpen" - // } - // } - ] + "version": "2.0.0", + "tasks": [ + { + "label": "echo", + "type": "shell", + "command": "echo ${workspaceFolder}" + } + ] } \ No newline at end of file diff --git a/firebase.json b/firebase.json index 136e335..a5a07b4 100644 --- a/firebase.json +++ b/firebase.json @@ -11,7 +11,6 @@ "rewrites": [ { "source": "**", - "destination": "/index.html", "run": { "serviceId": "pastelog", "region": "us-central1" diff --git a/package.json b/package.json index 935a425..46f30b9 100644 --- a/package.json +++ b/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "dev": "next dev", + "debug": "NODE_OPTIONS='--inspect' next dev", "build": "next build", "start": "next start", "lint": "next lint" @@ -57,4 +58,4 @@ "tailwindcss": "^3.4.1", "typescript": "^5" } -} +} \ No newline at end of file diff --git a/src/app/(main)/_components/Pastelog.tsx b/src/app/(main)/_components/Pastelog.tsx index 82306ca..9cdd8e1 100644 --- a/src/app/(main)/_components/Pastelog.tsx +++ b/src/app/(main)/_components/Pastelog.tsx @@ -118,6 +118,7 @@ export default function Pastelog({ id }: { id?: string }) { type: LogType.TEXT, title: title, createdDate: new Date(), + lastUpdatedAt: new Date(), isExpired: false, summary: '', isPublic: false, @@ -141,7 +142,6 @@ export default function Pastelog({ id }: { id?: string }) { } - async function handleImport(url: string) { setImportLoading(true); try { diff --git a/src/app/(main)/_components/PreviewPage.tsx b/src/app/(main)/_components/PreviewPage.tsx index d8ee973..f41a50a 100644 --- a/src/app/(main)/_components/PreviewPage.tsx +++ b/src/app/(main)/_components/PreviewPage.tsx @@ -75,6 +75,8 @@ const PreviewPage = ({ logId }: { logId: string }) => { const handleOnEdit = async (hasUpdated: boolean) => { setLoading(true); if (hasUpdated) { + // update last updated date + editedLog!.lastUpdatedAt = new Date(); await logService.updateLog(logId, editedLog!); setpreviewLog(new Log({ ...editedLog! })); } else { diff --git a/src/app/(main)/_components/Sidebar.tsx b/src/app/(main)/_components/Sidebar.tsx index 6bb1f82..29dd809 100644 --- a/src/app/(main)/_components/Sidebar.tsx +++ b/src/app/(main)/_components/Sidebar.tsx @@ -1,8 +1,7 @@ "use client"; import PencilSquareIcon from '@heroicons/react/24/solid/PencilSquareIcon'; import { useRouter } from 'next/navigation'; -import React, { useCallback, useEffect, useState } from 'react'; -import useSmallScreen from '../_hooks/useSmallScreen'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import Log from "../_models/Log"; import Analytics from '../_services/Analytics'; import { AuthService } from '../_services/AuthService'; @@ -18,11 +17,16 @@ const Sidebar: React.FC = () => { const [loading, setLoading] = useState(true); const [logs, setLogs] = useState([]); const [refresh, setRefresh] = useState(false); + const [isFetching, setIsFetching] = useState(false); const router = useRouter(); - const isSmallScreen = useSmallScreen(); + const [hasMore, setHasMore] = useState(true); const authService = new AuthService(); const logService = new LogService(); const [isFirstLogin, setIsFirstLogin] = useState(false); + const [page, setPage] = useState(1); + const logsEndRef = useRef(null); + const initialFetchDone = useRef(false); + const onLogClick = useCallback((log: Log | null) => { if (log) { setSelected(log); @@ -45,41 +49,91 @@ const Sidebar: React.FC = () => { setLoading(false); }; - const fetchLogs = useCallback(async () => { + const fetchLogs = useCallback(async (pageNumber: number = 1, isRefresh: boolean = false) => { + console.log("fetching logs:"); + if (isFetching) return; + setIsFetching(true); setLoading(true); try { await logService.deleteExpiredLogs(); + let fetchedLogs: Log[] = []; if (user && user.uid) { const isFirstLogin = await authService.isFirstTimeLogin(user.uid); if (isFirstLogin) { - const logs = await logService.fetchLogsFromLocal(); - setLogs(logs); + fetchedLogs = await logService.fetchLogsFromLocal(); } else { - const fetchedLogs = await logService.getLogsByUserId(user.uid) - setLogs(fetchedLogs); + fetchedLogs = await logService.getLogsByUserId(user.uid, pageNumber); // Add pagination to this method } } else { - const logs = await logService.fetchLogsFromLocal(); - setLogs(logs); + fetchedLogs = await logService.fetchLogsFromLocal(); + } + + if (isRefresh) { + setLogs(fetchedLogs); + } else { + setLogs(prevLogs => [...prevLogs, ...fetchedLogs]); } + + setHasMore(fetchedLogs.length > 0); // Update hasMore based on fetched results setLoading(false); } catch (_) { setLoading(false); + } finally { + setIsFetching(false); } }, [user]); useEffect(() => { - const unsubscribe = authService.onAuthStateChanged((user) => { - setUser(user); - if (user) { - fetchLogs(); + const unsubscribe = authService.onAuthStateChanged((newUser) => { + setUser(newUser); + if (newUser && !initialFetchDone.current) { + fetchLogs(1, true); + setPage(1); + initialFetchDone.current = true; } }); - fetchLogs(); + + if (!initialFetchDone.current) { + fetchLogs(1, true); + initialFetchDone.current = true; + } + return () => unsubscribe(); - }, [fetchLogs, refresh]); + }, [fetchLogs, setUser]); - const handleRefresh = () => setRefresh(prev => !prev); + useEffect(() => { + const observer = new IntersectionObserver( + entries => { + if (entries[0].isIntersecting && hasMore && !loading && (user && user.uid)) { + setPage(prevPage => prevPage + 1); + } + }, + { threshold: 1.0 } + ); + + if (logsEndRef.current) { + observer.observe(logsEndRef.current); + } + + return () => { + if (logsEndRef.current) { + observer.unobserve(logsEndRef.current); + } + }; + }, [hasMore, loading]); + + + useEffect(() => { + if (page > 1) { + fetchLogs(page); + } + }, [page, fetchLogs]); + + const handleRefresh = useCallback(() => { + setRefresh(prev => !prev); + setPage(1); + fetchLogs(1, true); + }, [fetchLogs]); const handleLogin = async () => { try { @@ -94,9 +148,7 @@ const Sidebar: React.FC = () => { try { await authService.signOut(); setUser(null); - // Clear the logs state immediately setLogs([]); - // Fetch logs from local storage after logout const localLogs = await logService.fetchLogsFromLocal(); setLogs(localLogs); router.push('/logs'); @@ -120,11 +172,13 @@ const Sidebar: React.FC = () => { {/* Scrollable logs list */} - {loading ? ( -
-
-
- ) : + { + // loading ? ( + //
+ //
+ //
+ // ) : + (
{logs.map((log: Log) => ( { onRefresh={handleRefresh} // Pass the refresh function /> ))} + {loading &&
+

Loading...

+
} +
)} {/* */} diff --git a/src/app/(main)/_models/Log.ts b/src/app/(main)/_models/Log.ts index c5df1e0..4b92b75 100644 --- a/src/app/(main)/_models/Log.ts +++ b/src/app/(main)/_models/Log.ts @@ -10,6 +10,7 @@ export interface ILog { expiryDate: Date | null; data: string; createdDate: Date; + lastUpdatedAt: Date; title?: string | ''; type: LogType; isMarkDown: boolean; @@ -25,6 +26,7 @@ export class Log implements ILog { data: string; title?: string | ''; createdDate: Date; + lastUpdatedAt: Date; type: LogType; isMarkDown: boolean; isExpired?: boolean | false; @@ -37,6 +39,7 @@ export class Log implements ILog { expiryDate = null, data, createdDate = new Date(), + lastUpdatedAt = new Date(), type, isMarkDown, title = '', @@ -49,6 +52,7 @@ export class Log implements ILog { expiryDate?: Date | null, data: string, createdDate?: Date, + lastUpdatedAt?: Date, type: LogType, isMarkDown: boolean, title?: string, @@ -59,6 +63,7 @@ export class Log implements ILog { id?: string }) { this.expiryDate = expiryDate; + this.lastUpdatedAt = new Date(createdDate); this.data = data; this.title = title; this.isExpired = isExpired; @@ -75,6 +80,7 @@ export class Log implements ILog { const data = doc.data(); return new Log({ expiryDate: data.expiryDate ? new Date(data.expiryDate) : null, + lastUpdatedAt: new Date(data.lastUpdatedAt), data: data.data, createdDate: new Date(data.createdDate), type: data.type as LogType, @@ -92,6 +98,7 @@ export class Log implements ILog { toFirestore(): any { const doc: any = { expiryDate: this.expiryDate ? this.expiryDate.toISOString() : null, + lastUpdatedAt: this.lastUpdatedAt ? this.lastUpdatedAt.toISOString() : null, data: this.data, createdDate: this.createdDate.toISOString(), title: this.title ? this.title : '', diff --git a/src/app/(main)/_services/logService.ts b/src/app/(main)/_services/logService.ts index 6582313..ef3876c 100644 --- a/src/app/(main)/_services/logService.ts +++ b/src/app/(main)/_services/logService.ts @@ -2,7 +2,7 @@ import { GoogleGenerativeAI } from "@google/generative-ai"; import axios from 'axios'; -import { addDoc, collection, deleteDoc, doc, getDoc, getDocs, query, setDoc, updateDoc, where } from 'firebase/firestore'; +import { addDoc, collection, deleteDoc, doc, getDoc, getDocs, limit, orderBy, query, setDoc, startAfter, updateDoc, where } from 'firebase/firestore'; import { db } from '../../../utils/firebase'; import { Log, LogType } from '../_models/Log'; class LogService { @@ -42,41 +42,72 @@ class LogService { return querySnapshot.docs.map(doc => Log.fromFirestore(doc)); } - async getLogsByUserId(userId: string): Promise { - // Fetch user-specific logs with isExpired = false - const userQuery = query( + private async getLastVisibleDoc(userId: string, page: number, logsPerPage: number) { + const lastVisibleDocQuery = query( this.logCollection, where('userId', '==', userId), - where('isExpired', '==', false) + where('isExpired', '==', false), + orderBy('lastUpdatedAt', 'desc'), + limit((page) * logsPerPage) ); - const userQuerySnapshot = await getDocs(userQuery); - const userLogs = userQuerySnapshot.docs.map(doc => Log.fromFirestore(doc)); - - // Fetch public logs - const publicIdLogs = ['getting-started', 'shortcuts']; - const publicLogPromises = publicIdLogs.map(async (id) => { - const docRef = doc(this.logCollection, id); - const docSnap = await getDoc(docRef); - return docSnap.exists() ? Log.fromFirestore(docSnap) : null; - }); - const publicLogs = (await Promise.all(publicLogPromises)).filter((log): log is Log => log !== null); + const snapShot = await getDocs(lastVisibleDocQuery); + return snapShot.docs[snapShot.docs.length - 1]; + } - // Combine user logs and public logs - const combinedLogs = [...userLogs, ...publicLogs]; + async getLogsByUserId(userId: string, page: number = 1, logsPerPage: number = 10): Promise { + try { + // Create a base query + let baseQuery = query( + this.logCollection, + where('userId', '==', userId), + where('isExpired', '==', false), + orderBy('lastUpdatedAt', 'desc'), + limit(logsPerPage) + ); + + if (page > 1) { + // Get the last document from the previous page + const lastVisibleDoc = await this.getLastVisibleDoc(userId, page - 1, logsPerPage); + if (lastVisibleDoc) { + baseQuery = query(baseQuery, startAfter(lastVisibleDoc)); + } + } - // Remove duplicates (in case a user has a personal copy of a public log) - const uniqueLogs = combinedLogs.reduce((acc: Log[], current) => { - const x = acc.find(item => item.id === current.id); - if (!x) { - return acc.concat([current]); - } else { - return acc; + const userQuerySnapshot = await getDocs(baseQuery); + const userLogs = userQuerySnapshot.docs.map(doc => Log.fromFirestore(doc)); + + // Fetch public logs (only on the first page) + let publicLogs: Log[] = []; + if (page === 1) { + const publicIdLogs = ['getting-started', 'shortcuts']; + const publicLogPromises = publicIdLogs.map(async (id) => { + const docRef = doc(this.logCollection, id); + const docSnap = await getDoc(docRef); + return docSnap.exists() ? Log.fromFirestore(docSnap) : null; + }); + publicLogs = (await Promise.all(publicLogPromises)).filter((log): log is Log => log !== null); } - }, []); - // Sort logs by createdDate in descending order - return uniqueLogs.sort((a, b) => b.createdDate.getTime() - a.createdDate.getTime()); + // Combine user logs and public logs + const combinedLogs = [...userLogs, ...publicLogs]; + + // Remove duplicates + const uniqueLogs = combinedLogs.reduce((acc: Log[], current) => { + const x = acc.find(item => item.id === current.id); + if (!x) { + return acc.concat([current]); + } else { + return acc; + } + }, []); + + // Sort logs by createdDate + return uniqueLogs.sort((a, b) => b.createdDate.getTime() - a.createdDate.getTime()); + } catch (error) { + console.error("Error in getLogsByUserId:", error); + throw error; + } } async publishLog(log: Log): Promise { @@ -142,7 +173,9 @@ class LogService { async updateLogTitle(id: string, log: Log): Promise { const docRef = doc(this.logCollection, id); - await updateDoc(docRef, { title: log.title }); + await updateDoc(docRef, { + title: log.title + }); await this.saveLogToLocal(log); } diff --git a/src/utils/firebase.ts b/src/utils/firebase.ts index bab8cc3..47333d5 100644 --- a/src/utils/firebase.ts +++ b/src/utils/firebase.ts @@ -13,8 +13,8 @@ const firebaseConfig = { }; const app = initializeApp(firebaseConfig); -const db = getFirestore(app); const auth = getAuth(app); +const db = getFirestore(app); const analytics = typeof window !== 'undefined' ? getAnalytics(app) : null; export { analytics, auth, db };