From 9b761210f014042c20a5f533e6865990038250c9 Mon Sep 17 00:00:00 2001 From: akashamba Date: Tue, 3 Jun 2025 13:08:08 -0400 Subject: [PATCH 1/6] make card clckble --- src/components/ArticleList.tsx | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/components/ArticleList.tsx b/src/components/ArticleList.tsx index 6dcaef0..c172934 100644 --- a/src/components/ArticleList.tsx +++ b/src/components/ArticleList.tsx @@ -1,6 +1,7 @@ import type { Article } from "@/types/articles"; import React from "react"; import { Button } from "./ui/button"; +import Link from "next/link"; interface ArticleListProps { articles: Article[]; @@ -17,12 +18,18 @@ export default function ArticleList({ return (
{articles.map((article: Article) => ( -
- {article.title}: {article.description} - -
+ +
+ {article.title} + {article.description ?? `: ${article.description}`} + +
+ ))}
); From dea3df51430324f6e0b484c6fbf281d228ad1c8a Mon Sep 17 00:00:00 2001 From: akashamba Date: Tue, 3 Jun 2025 22:29:40 -0400 Subject: [PATCH 2/6] fix description formatting --- src/components/ArticleList.tsx | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/components/ArticleList.tsx b/src/components/ArticleList.tsx index c172934..9789930 100644 --- a/src/components/ArticleList.tsx +++ b/src/components/ArticleList.tsx @@ -18,18 +18,17 @@ export default function ArticleList({ return (
{articles.map((article: Article) => ( - -
- {article.title} - {article.description ?? `: ${article.description}`} - -
- +
+ +
+ {article.title} + {article.description && ` : ${article.description}`} + +
+ +
))}
); From 13f83a597c60887bcddefc8d80831290184fa725 Mon Sep 17 00:00:00 2001 From: akashamba Date: Wed, 4 Jun 2025 02:24:13 -0400 Subject: [PATCH 3/6] [do not push] midway through integrating new ui, committing to mess with db --- bun.lock | 8 + package.json | 3 + src/app/articles/loading.tsx | 3 + src/app/articles/page.tsx | 209 ++++++++++++++ src/app/layout.tsx | 27 +- src/app/loading.tsx | 9 + src/app/not-found.tsx | 34 +++ src/app/page.tsx | 93 +++++-- src/components/article-card.tsx | 95 +++++++ src/components/article-list.tsx | 29 ++ src/components/footer.tsx | 26 ++ src/components/loading-spinner.tsx | 21 ++ src/components/modals/edit-article-modal.tsx | 157 +++++++++++ src/components/modals/new-article-modal.tsx | 186 +++++++++++++ src/components/navbar.tsx | 183 ++++++++++++- .../{ArticleList.tsx => old:ArticleList.tsx} | 0 .../{HomePage.tsx => old:HomePage.tsx} | 2 +- .../{LandingPage.tsx => old:LandingPage.tsx} | 0 src/components/theme-provider.tsx | 11 + src/components/ui/alert-dialog.tsx | 157 +++++++++++ src/components/ui/avatar.tsx | 53 ++++ src/components/ui/card.tsx | 79 ++++++ src/components/ui/dropdown-menu.tsx | 257 ++++++++++++++++++ src/components/ui/textarea.tsx | 18 ++ src/lib/types.ts | 30 ++ src/server/api/routers/articles.ts | 10 +- src/server/db/schema.ts | 6 +- src/types/articles.ts | 9 - 28 files changed, 1663 insertions(+), 52 deletions(-) create mode 100644 src/app/articles/loading.tsx create mode 100644 src/app/articles/page.tsx create mode 100644 src/app/loading.tsx create mode 100644 src/app/not-found.tsx create mode 100644 src/components/article-card.tsx create mode 100644 src/components/article-list.tsx create mode 100644 src/components/footer.tsx create mode 100644 src/components/loading-spinner.tsx create mode 100644 src/components/modals/edit-article-modal.tsx create mode 100644 src/components/modals/new-article-modal.tsx rename src/components/{ArticleList.tsx => old:ArticleList.tsx} (100%) rename src/components/{HomePage.tsx => old:HomePage.tsx} (98%) rename src/components/{LandingPage.tsx => old:LandingPage.tsx} (100%) create mode 100644 src/components/theme-provider.tsx create mode 100644 src/components/ui/alert-dialog.tsx create mode 100644 src/components/ui/avatar.tsx create mode 100644 src/components/ui/card.tsx create mode 100644 src/components/ui/dropdown-menu.tsx create mode 100644 src/components/ui/textarea.tsx create mode 100644 src/lib/types.ts delete mode 100644 src/types/articles.ts diff --git a/bun.lock b/bun.lock index ee00c7b..7110137 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,7 @@ "dependencies": { "@clerk/nextjs": "^6.20.2", "@neondatabase/serverless": "neondatabase/serverless", + "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", @@ -20,6 +21,7 @@ "drizzle-orm": "^0.44.1", "lucide-react": "^0.511.0", "next": "^15.2.3", + "next-themes": "^0.4.6", "postgres": "^3.4.7", "react": "^19.0.0", "react-dom": "^19.0.0", @@ -249,6 +251,8 @@ "@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="], + "@radix-ui/react-avatar": ["@radix-ui/react-avatar@1.1.10", "", { "dependencies": { "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-is-hydrated": "0.1.0", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-V8piFfWapM5OmNCXTzVQY+E1rDa53zY+MQ4Y7356v4fFz6vqCyUtIz2rUD44ZEdwg78/jKmMJHj07+C/Z/rcog=="], + "@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="], "@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="], @@ -293,6 +297,8 @@ "@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="], + "@radix-ui/react-use-is-hydrated": ["@radix-ui/react-use-is-hydrated@0.1.0", "", { "dependencies": { "use-sync-external-store": "^1.5.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-U+UORVEq+cTnRIaostJv9AGdV3G6Y+zbVd+12e18jQ5A3c0xL03IhnHuiU4UV69wolOQp5GfR58NW/EgdQhwOA=="], + "@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="], "@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="], @@ -829,6 +835,8 @@ "next": ["next@15.3.3", "", { "dependencies": { "@next/env": "15.3.3", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.3", "@next/swc-darwin-x64": "15.3.3", "@next/swc-linux-arm64-gnu": "15.3.3", "@next/swc-linux-arm64-musl": "15.3.3", "@next/swc-linux-x64-gnu": "15.3.3", "@next/swc-linux-x64-musl": "15.3.3", "@next/swc-win32-arm64-msvc": "15.3.3", "@next/swc-win32-x64-msvc": "15.3.3", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw=="], + "next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="], + "no-case": ["no-case@3.0.4", "", { "dependencies": { "lower-case": "^2.0.2", "tslib": "^2.0.3" } }, "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], diff --git a/package.json b/package.json index c7a9f5c..aa94d75 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,8 @@ "dependencies": { "@clerk/nextjs": "^6.20.2", "@neondatabase/serverless": "neondatabase/serverless", + "@radix-ui/react-alert-dialog": "^1.1.14", + "@radix-ui/react-avatar": "^1.1.10", "@radix-ui/react-dialog": "^1.1.14", "@radix-ui/react-dropdown-menu": "^2.1.15", "@radix-ui/react-label": "^2.1.7", @@ -36,6 +38,7 @@ "drizzle-orm": "^0.44.1", "lucide-react": "^0.511.0", "next": "^15.2.3", + "next-themes": "^0.4.6", "postgres": "^3.4.7", "react": "^19.0.0", "react-dom": "^19.0.0", diff --git a/src/app/articles/loading.tsx b/src/app/articles/loading.tsx new file mode 100644 index 0000000..f15322a --- /dev/null +++ b/src/app/articles/loading.tsx @@ -0,0 +1,3 @@ +export default function Loading() { + return null +} diff --git a/src/app/articles/page.tsx b/src/app/articles/page.tsx new file mode 100644 index 0000000..a31e82e --- /dev/null +++ b/src/app/articles/page.tsx @@ -0,0 +1,209 @@ +"use client"; + +import { useState, useMemo } from "react"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Plus, Search } from "lucide-react"; +import { ArticleList } from "@/components/article-list"; +import { NewArticleModal } from "@/components/modals/new-article-modal"; +import { EditArticleModal } from "@/components/modals/edit-article-modal"; +import type { Article } from "@/lib/types"; +import { api } from "@/trpc/react"; + +// Mock articles data +const mockArticles: Article[] = [ + { + id: "1", + url: "https://example.com/article-1", + title: "The Future of Web Development", + description: + "Exploring the latest trends and technologies shaping the future of web development.", + imageUrl: "/placeholder.svg?height=200&width=300", + createdAt: new Date("2024-01-15"), + updatedAt: new Date("2024-01-15"), + userId: "user-abc", + tags: "", + }, + { + id: "2", + url: "https://example.com/article-2", + title: "Understanding React Server Components", + description: + "A deep dive into React Server Components and how they change the way we build applications.", + imageUrl: "/placeholder.svg?height=200&width=300", + createdAt: new Date("2024-01-14"), + updatedAt: new Date("2024-01-14"), + userId: "user-abc", + tags: "", + }, + { + id: "3", + url: "https://example.com/article-3", + title: "CSS Grid vs Flexbox: When to Use Which", + description: + "A comprehensive guide to choosing between CSS Grid and Flexbox for your layouts.", + imageUrl: "/placeholder.svg?height=200&width=300", + createdAt: new Date("2024-01-13"), + updatedAt: new Date("2024-01-13"), + userId: "user-abc", + tags: "", + }, +]; + +export default function ArticlesPage() { + const { + data: articles, + isLoading, + isError, + error, + } = api.articles.getAll.useQuery(); + const [searchQuery, setSearchQuery] = useState(""); + const [isNewArticleModalOpen, setIsNewArticleModalOpen] = useState(false); + const [isEditArticleModalOpen, setIsEditArticleModalOpen] = useState(false); + const [editingArticle, setEditingArticle] = useState
(null); + const utils = api.useUtils(); + + const deleteArticle = api.articles.delete.useMutation({ + onSuccess: () => { + // Invalidate and refetch articles query + utils.articles.getAll.invalidate().catch((error) => { + console.error("Failed to invalidate cache:", error); + }); + console.log("Article deleted successfully!"); + }, + onError: (error) => { + console.log(`Failed to delete article: ${error.message}`); + }, + }); + + const filteredArticles = useMemo((): Article[] => { + if (!articles) return []; + return articles.filter((article) => { + const matchesSearch = article.title + .toLowerCase() + .includes(searchQuery.toLowerCase()); + + return matchesSearch; + }); + }, [articles, searchQuery]); + + const handleNewArticle = () => { + // Refresh articles list - in real app, this would refetch from API + console.log("Refreshing articles list..."); + }; + + const handleEditArticle = (article: Article) => { + setEditingArticle(article); + setIsEditArticleModalOpen(true); + }; + + const handleUpdateArticle = () => { + // Refresh articles list - in real app, this would refetch from API + console.log("Refreshing articles list after update..."); + }; + + const handleDeleteArticle = async (id: string) => { + try { + deleteArticle.mutateAsync({ id: id }); + } catch (error) { + console.log("Failed to delete article", error); + } + }; + + if (isLoading) { + return ( +
+

Loading articles...

+
+ ); + } + + if (isError) { + return ( +
+

Error loading articles: {error?.message}

+ +
+ ); + } + + if (!articles) { + return
Something went wrong
; + } + + return ( +
+ {/* Header */} +
+
+

My Articles

+

+ {articles.length} article{articles.length !== 1 ? "s" : ""} saved +

+
+ +
+ + {/* Search Bar */} +
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+ + {/* Articles List or Empty State */} + {articles.length === 0 ? ( +
+
📚
+

+ No articles yet +

+

+ Add your first article to get started! +

+ +
+ ) : ( + + )} + + {/* Modals */} + setIsNewArticleModalOpen(false)} + onSave={handleNewArticle} + /> + + { + setIsEditArticleModalOpen(false); + setEditingArticle(null); + }} + onSave={handleUpdateArticle} + /> +
+ ); +} diff --git a/src/app/layout.tsx b/src/app/layout.tsx index eba9f62..d813161 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,25 +1,36 @@ +import type React from "react"; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; import "@/styles/globals.css"; - -import { type Metadata } from "next"; - +import { Navbar } from "@/components/navbar"; +import { Footer } from "@/components/footer"; +import { ClerkProvider } from "@clerk/nextjs"; import { TRPCReactProvider } from "@/trpc/react"; -import { ClerkProvider } from "@clerk/nextjs"; +const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { - title: "Create T3 App", - description: "Generated by create-t3-app", + title: "Flipside - Save and Organize Your Articles", + description: "Save articles from URLs and organize them in one place", icons: [{ rel: "icon", url: "/favicon.png" }], }; export default function RootLayout({ children, -}: Readonly<{ children: React.ReactNode }>) { +}: { + children: React.ReactNode; +}) { return ( - {children} + +
+ +
{children}
+
+
+
diff --git a/src/app/loading.tsx b/src/app/loading.tsx new file mode 100644 index 0000000..2b7b503 --- /dev/null +++ b/src/app/loading.tsx @@ -0,0 +1,9 @@ +import { LoadingSpinner } from "@/components/loading-spinner" + +export default function Loading() { + return ( +
+ +
+ ) +} diff --git a/src/app/not-found.tsx b/src/app/not-found.tsx new file mode 100644 index 0000000..350e37c --- /dev/null +++ b/src/app/not-found.tsx @@ -0,0 +1,34 @@ +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { Home, ArrowLeft } from "lucide-react" + +export default function NotFound() { + return ( +
+
+
+
404
+

Page Not Found

+

+ Oops! The page you're looking for doesn't exist. It might have been moved or deleted. +

+
+ +
+ + +
+
+
+ ) +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 68a53a3..c3cd752 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,19 +1,80 @@ -// import { api, HydrateClient } from "@/trpc/server"; -import { SignedIn, SignedOut } from "@clerk/nextjs"; -import Navbar from "../components/navbar"; -import HomePage from "@/components/HomePage"; -import LandingPage from "@/components/LandingPage"; +import Link from "next/link" +import { Button } from "@/components/ui/button" +import { Search, Upload, Zap } from "lucide-react" -export default async function Home() { +export default function LandingPage() { return ( - <> - - - - - - - - - ); +
+ {/* Hero Section */} +
+
+

+ Save Articles, Stay Organized +

+

+ Never lose track of interesting articles again. Save them with a simple URL, organize them beautifully, and + access them anywhere. +

+ +
+
+ + {/* Features Section */} +
+
+
+

+ Everything you need to manage articles +

+

+ Simple, powerful tools to help you save, organize, and rediscover your favorite content. +

+
+ +
+
+
+ +
+

Lightning Fast

+

+ Just paste a URL and we'll automatically extract the title, description, and image. +

+
+ +
+
+ +
+

Smart Search

+

Find any article instantly with our powerful search functionality.

+
+ +
+
+ +
+

Bulk Import

+

Import hundreds of articles at once from a CSV file.

+
+
+
+
+ + {/* CTA Section */} +
+
+

Ready to get organized?

+

+ Join thousands of users who have already organized their reading with ArticleSaver. +

+ +
+
+
+ ) } diff --git a/src/components/article-card.tsx b/src/components/article-card.tsx new file mode 100644 index 0000000..8535e50 --- /dev/null +++ b/src/components/article-card.tsx @@ -0,0 +1,95 @@ +"use client" + +import Image from "next/image" +import type { Article } from "@/lib/types" +import { Button } from "@/components/ui/button" +import { Card, CardContent } from "@/components/ui/card" +import { Edit, Trash2, ExternalLink } from "lucide-react" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" + +interface ArticleCardProps { + article: Article + onEdit: (article: Article) => void + onDelete: (id: string) => void +} + +export function ArticleCard({ article, onEdit, onDelete }: ArticleCardProps) { + const handleDelete = () => { + onDelete(article.id) + } + + return ( + + +
+ {/* Image */} +
+ {article.title} +
+ + {/* Content */} +
+

{article.title}

+

{article.description}

+
+ + {/* Actions */} +
+ + +
+ + + + + + + + + Delete Article + + Are you sure you want to delete "{article.title}"? This action cannot be undone. + + + + Cancel + Delete + + + +
+
+
+
+
+ ) +} diff --git a/src/components/article-list.tsx b/src/components/article-list.tsx new file mode 100644 index 0000000..3737e69 --- /dev/null +++ b/src/components/article-list.tsx @@ -0,0 +1,29 @@ +"use client" + +import type { Article } from "@/lib/types" +import { ArticleCard } from "./article-card" + +interface ArticleListProps { + articles: Article[] + onEdit: (article: Article) => void + onDelete: (id: string) => void +} + +export function ArticleList({ articles, onEdit, onDelete }: ArticleListProps) { + if (articles.length === 0) { + return ( +
+
No articles found
+

Try adjusting your search or add a new article

+
+ ) + } + + return ( +
+ {articles.map((article) => ( + + ))} +
+ ) +} diff --git a/src/components/footer.tsx b/src/components/footer.tsx new file mode 100644 index 0000000..7166fd2 --- /dev/null +++ b/src/components/footer.tsx @@ -0,0 +1,26 @@ +import Link from "next/link" + +export function Footer() { + return ( +
+
+
+
+ © {new Date().getFullYear()} ArticleSaver. All rights reserved. +
+
+ + About + + + Contact + + + Privacy + +
+
+
+
+ ) +} diff --git a/src/components/loading-spinner.tsx b/src/components/loading-spinner.tsx new file mode 100644 index 0000000..1de3be5 --- /dev/null +++ b/src/components/loading-spinner.tsx @@ -0,0 +1,21 @@ +import { Loader2 } from "lucide-react" + +interface LoadingSpinnerProps { + size?: "sm" | "md" | "lg" + text?: string +} + +export function LoadingSpinner({ size = "md", text }: LoadingSpinnerProps) { + const sizeClasses = { + sm: "h-4 w-4", + md: "h-8 w-8", + lg: "h-12 w-12", + } + + return ( +
+ + {text &&

{text}

} +
+ ) +} diff --git a/src/components/modals/edit-article-modal.tsx b/src/components/modals/edit-article-modal.tsx new file mode 100644 index 0000000..2d92930 --- /dev/null +++ b/src/components/modals/edit-article-modal.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Textarea } from "@/components/ui/textarea"; +import { Loader2 } from "lucide-react"; +import type { Article } from "@/lib/types"; +import Image from "next/image"; +import { api } from "@/trpc/react"; + +interface EditArticleModalProps { + isOpen: boolean; + article: Article | null; + onClose: () => void; + onSave: () => void; +} + +export function EditArticleModal({ + isOpen, + article, + onClose, + onSave, +}: EditArticleModalProps) { + const [title, setTitle] = useState(""); + const [description, setDescription] = useState(""); + const [tags, setTags] = useState(""); + const [isSaving, setIsSaving] = useState(false); + const utils = api.useUtils(); + + const updateArticle = api.articles.updateArticle.useMutation({ + onSuccess: () => { + // Invalidate and refetch articles query + utils.articles.getAll.invalidate().catch((error) => { + console.error("Failed to invalidate cache:", error); + }); + console.log("Article created successfully!"); + }, + onError: (error) => { + console.log(`Failed to create article: ${error.message}`); + }, + }); + + useEffect(() => { + if (article) { + setTitle(article.title); + setDescription(article.description ?? ""); + } + }, [article]); + + const handleSave = async () => { + if (!article || !title) return; + + setIsSaving(true); + try { + await updateArticle.mutateAsync({ + id: article.id, + title: title, + tags: tags || article.tags, + }); + + onSave(); + onClose(); + } catch (error) { + console.error("Failed to update article:", error); + } finally { + setIsSaving(false); + } + }; + + const handleClose = () => { + setTitle(""); + setDescription(""); + onClose(); + }; + + if (!article) return null; + + return ( + + + + Edit Article + + Update the title and description for this article. + + + +
+
+ + +
+ +
+ + setTitle(e.target.value)} + /> +
+ +
+ +