Last active
January 2, 2026 01:23
-
-
Save paulweezydesign/b2d4dbe20d0621cc0a70410c74120224 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React from 'react'; | |
| const UserNotRegisteredError = () => { | |
| return ( | |
| <div className="flex flex-col items-center justify-center min-h-screen bg-gradient-to-b from-white to-slate-50"> | |
| <div className="max-w-md w-full p-8 bg-white rounded-lg shadow-lg border border-slate-100"> | |
| <div className="text-center"> | |
| <div className="inline-flex items-center justify-center w-16 h-16 mb-6 rounded-full bg-orange-100"> | |
| <svg className="w-8 h-8 text-orange-600" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | |
| <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> | |
| </svg> | |
| </div> | |
| <h1 className="text-3xl font-bold text-slate-900 mb-4">Access Restricted</h1> | |
| <p className="text-slate-600 mb-8"> | |
| You are not registered to use this application. Please contact the app administrator to request access. | |
| </p> | |
| <div className="p-4 bg-slate-50 rounded-md text-sm text-slate-600"> | |
| <p>If you believe this is an error, you can:</p> | |
| <ul className="list-disc list-inside mt-2 space-y-1"> | |
| <li>Verify you are logged in with the correct account</li> | |
| <li>Contact the app administrator for access</li> | |
| <li>Try logging out and back in again</li> | |
| </ul> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| ); | |
| }; | |
| export default UserNotRegisteredError; | |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useState, useEffect } from 'react'; | |
| import { useQuery, useQueryClient } from '@tanstack/react-query'; | |
| import { base44 } from '@/api/base44Client'; | |
| import { Button } from "@/components/ui/button"; | |
| import { Badge } from "@/components/ui/badge"; | |
| import { Skeleton } from "@/components/ui/skeleton"; | |
| import { | |
| ArrowLeft, Hash, Eye, Clock, User, Pencil, Trash2, | |
| ChevronRight, BookOpen | |
| } from 'lucide-react'; | |
| import { motion } from 'framer-motion'; | |
| import { Link, useNavigate } from 'react-router-dom'; | |
| import { createPageUrl } from '@/utils'; | |
| import { format } from 'date-fns'; | |
| import ContentBlock from '@/components/knowledge/ContentBlock'; | |
| import FeedbackActions from '@/components/knowledge/FeedbackActions'; | |
| import ArticleEditor from '@/components/knowledge/ArticleEditor'; | |
| import CategoryTree from '@/components/knowledge/CategoryTree'; | |
| export default function Article() { | |
| const urlParams = new URLSearchParams(window.location.search); | |
| const articleId = urlParams.get('id'); | |
| const navigate = useNavigate(); | |
| const queryClient = useQueryClient(); | |
| const [editorOpen, setEditorOpen] = useState(false); | |
| const { data: article, isLoading } = useQuery({ | |
| queryKey: ['article', articleId], | |
| queryFn: async () => { | |
| const articles = await base44.entities.Article.filter({ id: articleId }); | |
| const art = articles[0]; | |
| if (art) { | |
| await base44.entities.Article.update(art.id, { | |
| view_count: (art.view_count || 0) + 1 | |
| }); | |
| } | |
| return art; | |
| }, | |
| enabled: !!articleId | |
| }); | |
| const { data: categories = [] } = useQuery({ | |
| queryKey: ['categories'], | |
| queryFn: () => base44.entities.Category.list() | |
| }); | |
| const { data: allArticles = [] } = useQuery({ | |
| queryKey: ['articles'], | |
| queryFn: () => base44.entities.Article.list() | |
| }); | |
| const category = categories.find(c => c.id === article?.category_id); | |
| const relatedArticles = allArticles | |
| .filter(a => a.category_id === article?.category_id && a.id !== article?.id) | |
| .slice(0, 3); | |
| const handleUpdateArticle = async (data) => { | |
| await base44.entities.Article.update(article.id, data); | |
| queryClient.invalidateQueries({ queryKey: ['article', articleId] }); | |
| queryClient.invalidateQueries({ queryKey: ['articles'] }); | |
| }; | |
| const handleDelete = async () => { | |
| if (confirm('Are you sure you want to delete this article?')) { | |
| await base44.entities.Article.delete(article.id); | |
| navigate(createPageUrl('KnowledgeBase')); | |
| } | |
| }; | |
| if (isLoading) { | |
| return ( | |
| <div className="min-h-screen bg-slate-50 flex"> | |
| <aside className="hidden lg:block w-72 bg-white border-r border-slate-200 p-6"> | |
| <Skeleton className="h-8 w-40 mb-4" /> | |
| <Skeleton className="h-6 w-full mb-2" /> | |
| <Skeleton className="h-6 w-full mb-2" /> | |
| <Skeleton className="h-6 w-3/4" /> | |
| </aside> | |
| <main className="flex-1 p-8"> | |
| <div className="max-w-3xl mx-auto"> | |
| <Skeleton className="h-10 w-3/4 mb-4" /> | |
| <Skeleton className="h-6 w-full mb-2" /> | |
| <Skeleton className="h-6 w-full mb-2" /> | |
| <Skeleton className="h-6 w-2/3" /> | |
| </div> | |
| </main> | |
| </div> | |
| ); | |
| } | |
| if (!article) { | |
| return ( | |
| <div className="min-h-screen bg-slate-50 flex items-center justify-center"> | |
| <div className="text-center"> | |
| <div className="w-20 h-20 bg-slate-200 rounded-2xl flex items-center justify-center mx-auto mb-4"> | |
| <BookOpen className="h-10 w-10 text-slate-400" /> | |
| </div> | |
| <h2 className="text-xl font-semibold text-slate-700 mb-2">Article not found</h2> | |
| <p className="text-slate-500 mb-6">The article you're looking for doesn't exist</p> | |
| <Link to={createPageUrl('KnowledgeBase')}> | |
| <Button className="gap-2"> | |
| <ArrowLeft className="h-4 w-4" /> | |
| Back to Knowledge Base | |
| </Button> | |
| </Link> | |
| </div> | |
| </div> | |
| ); | |
| } | |
| return ( | |
| <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50 flex"> | |
| {/* Sidebar */} | |
| <aside className="hidden lg:flex flex-col w-72 bg-white border-r border-slate-200 sticky top-0 h-screen"> | |
| <div className="p-6 border-b border-slate-100"> | |
| <Link | |
| to={createPageUrl('KnowledgeBase')} | |
| className="flex items-center gap-3 text-slate-600 hover:text-slate-900 transition-colors" | |
| > | |
| <div className="p-2 bg-slate-100 rounded-xl"> | |
| <BookOpen className="h-5 w-5" /> | |
| </div> | |
| <span className="font-semibold">Knowledge Base</span> | |
| </Link> | |
| </div> | |
| <div className="flex-1 overflow-y-auto p-4"> | |
| <h3 className="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-3"> | |
| Navigation | |
| </h3> | |
| <CategoryTree | |
| categories={categories} | |
| articles={allArticles} | |
| selectedArticleId={articleId} | |
| /> | |
| </div> | |
| </aside> | |
| {/* Main Content */} | |
| <main className="flex-1 min-w-0"> | |
| <div className="max-w-3xl mx-auto px-6 py-12"> | |
| {/* Breadcrumb */} | |
| <nav className="flex items-center gap-2 text-sm text-slate-500 mb-8"> | |
| <Link to={createPageUrl('KnowledgeBase')} className="hover:text-slate-700"> | |
| Home | |
| </Link> | |
| <ChevronRight className="h-4 w-4" /> | |
| {category && ( | |
| <> | |
| <span className="hover:text-slate-700 cursor-pointer">{category.name}</span> | |
| <ChevronRight className="h-4 w-4" /> | |
| </> | |
| )} | |
| <span className="text-slate-900 font-medium truncate">{article.title}</span> | |
| </nav> | |
| {/* Article Header */} | |
| <motion.header | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="mb-10" | |
| > | |
| <div className="flex items-start justify-between gap-4 mb-6"> | |
| <h1 className="text-3xl md:text-4xl font-bold text-slate-900 tracking-tight leading-tight"> | |
| {article.title} | |
| </h1> | |
| <div className="flex gap-2 shrink-0"> | |
| <Button | |
| variant="outline" | |
| size="icon" | |
| onClick={() => setEditorOpen(true)} | |
| className="rounded-xl" | |
| > | |
| <Pencil className="h-4 w-4" /> | |
| </Button> | |
| <Button | |
| variant="outline" | |
| size="icon" | |
| onClick={handleDelete} | |
| className="rounded-xl text-red-500 hover:text-red-600 hover:bg-red-50" | |
| > | |
| <Trash2 className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| </div> | |
| {article.summary && ( | |
| <p className="text-lg text-slate-600 mb-6 leading-relaxed"> | |
| {article.summary} | |
| </p> | |
| )} | |
| <div className="flex flex-wrap items-center gap-4 text-sm text-slate-500"> | |
| {article.created_by && ( | |
| <div className="flex items-center gap-1.5"> | |
| <User className="h-4 w-4" /> | |
| <span>{article.created_by}</span> | |
| </div> | |
| )} | |
| <div className="flex items-center gap-1.5"> | |
| <Clock className="h-4 w-4" /> | |
| <span>{format(new Date(article.updated_date || article.created_date), 'MMM d, yyyy')}</span> | |
| </div> | |
| <div className="flex items-center gap-1.5"> | |
| <Eye className="h-4 w-4" /> | |
| <span>{article.view_count || 0} views</span> | |
| </div> | |
| </div> | |
| {article.tags?.length > 0 && ( | |
| <div className="flex flex-wrap gap-2 mt-6"> | |
| {article.tags.map((tag, idx) => ( | |
| <Badge | |
| key={idx} | |
| variant="secondary" | |
| className="bg-slate-100 text-slate-600 hover:bg-slate-200" | |
| > | |
| <Hash className="h-3 w-3 mr-1" /> | |
| {tag} | |
| </Badge> | |
| ))} | |
| </div> | |
| )} | |
| </motion.header> | |
| {/* Article Content */} | |
| <motion.article | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.1 }} | |
| className="mb-12" | |
| > | |
| {article.content_blocks?.length > 0 ? ( | |
| <div className="space-y-4"> | |
| {article.content_blocks.map((block, idx) => ( | |
| <ContentBlock key={idx} block={block} /> | |
| ))} | |
| </div> | |
| ) : article.content ? ( | |
| <ContentBlock block={{ type: 'markdown', content: article.content }} /> | |
| ) : ( | |
| <div className="text-center py-12 text-slate-400"> | |
| This article has no content yet | |
| </div> | |
| )} | |
| </motion.article> | |
| {/* Feedback Actions */} | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.2 }} | |
| className="py-8 border-t border-slate-200" | |
| > | |
| <p className="text-sm text-slate-500 mb-4">Was this article helpful?</p> | |
| <FeedbackActions | |
| article={article} | |
| onHelpfulUpdate={() => queryClient.invalidateQueries({ queryKey: ['article', articleId] })} | |
| /> | |
| </motion.div> | |
| {/* Related Articles */} | |
| {relatedArticles.length > 0 && ( | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.3 }} | |
| className="py-8 border-t border-slate-200" | |
| > | |
| <h3 className="text-lg font-semibold text-slate-900 mb-4">Related Articles</h3> | |
| <div className="grid gap-3"> | |
| {relatedArticles.map(related => ( | |
| <Link | |
| key={related.id} | |
| to={createPageUrl('Article') + `?id=${related.id}`} | |
| className="flex items-center gap-3 p-4 bg-white rounded-xl border border-slate-100 hover:border-slate-200 hover:shadow-sm transition-all" | |
| > | |
| <div className="p-2 bg-slate-100 rounded-lg"> | |
| <BookOpen className="h-4 w-4 text-slate-600" /> | |
| </div> | |
| <div className="min-w-0"> | |
| <h4 className="font-medium text-slate-900 truncate">{related.title}</h4> | |
| {related.summary && ( | |
| <p className="text-sm text-slate-500 truncate">{related.summary}</p> | |
| )} | |
| </div> | |
| </Link> | |
| ))} | |
| </div> | |
| </motion.div> | |
| )} | |
| </div> | |
| </main> | |
| {/* Article Editor */} | |
| <ArticleEditor | |
| article={article} | |
| categories={categories} | |
| open={editorOpen} | |
| onClose={() => setEditorOpen(false)} | |
| onSave={handleUpdateArticle} | |
| /> | |
| </div> | |
| ); | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import React, { useState } from 'react'; | |
| import { useQuery, useQueryClient } from '@tanstack/react-query'; | |
| import { base44 } from '@/api/base44Client'; | |
| import { Button } from "@/components/ui/button"; | |
| import { Input } from "@/components/ui/input"; | |
| import { Dialog, DialogContent } from "@/components/ui/dialog"; | |
| import { | |
| Search, Plus, BookOpen, Sparkles, FileText, | |
| FolderPlus, Settings, Menu, X | |
| } from 'lucide-react'; | |
| import { motion, AnimatePresence } from 'framer-motion'; | |
| import { Link } from 'react-router-dom'; | |
| import { createPageUrl } from '@/utils'; | |
| import SearchBar from '@/components/knowledge/SearchBar'; | |
| import CategoryTree from '@/components/knowledge/CategoryTree'; | |
| import ArticleEditor from '@/components/knowledge/ArticleEditor'; | |
| export default function KnowledgeBase() { | |
| const [searchOpen, setSearchOpen] = useState(false); | |
| const [editorOpen, setEditorOpen] = useState(false); | |
| const [categoryModalOpen, setCategoryModalOpen] = useState(false); | |
| const [newCategoryName, setNewCategoryName] = useState(''); | |
| const [selectedParentId, setSelectedParentId] = useState(''); | |
| const [sidebarOpen, setSidebarOpen] = useState(false); | |
| const queryClient = useQueryClient(); | |
| const { data: categories = [], isLoading: loadingCategories } = useQuery({ | |
| queryKey: ['categories'], | |
| queryFn: () => base44.entities.Category.list() | |
| }); | |
| const { data: articles = [], isLoading: loadingArticles } = useQuery({ | |
| queryKey: ['articles'], | |
| queryFn: () => base44.entities.Article.list() | |
| }); | |
| const handleCreateCategory = async () => { | |
| if (!newCategoryName.trim()) return; | |
| await base44.entities.Category.create({ | |
| name: newCategoryName.trim(), | |
| parent_id: selectedParentId || null | |
| }); | |
| queryClient.invalidateQueries({ queryKey: ['categories'] }); | |
| setNewCategoryName(''); | |
| setSelectedParentId(''); | |
| setCategoryModalOpen(false); | |
| }; | |
| const handleSaveArticle = async (data) => { | |
| await base44.entities.Article.create(data); | |
| queryClient.invalidateQueries({ queryKey: ['articles'] }); | |
| }; | |
| const recentArticles = [...articles] | |
| .sort((a, b) => new Date(b.created_date) - new Date(a.created_date)) | |
| .slice(0, 5); | |
| const popularArticles = [...articles] | |
| .sort((a, b) => (b.helpful_count || 0) - (a.helpful_count || 0)) | |
| .slice(0, 5); | |
| return ( | |
| <div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-slate-50"> | |
| {/* Mobile Sidebar Toggle */} | |
| <button | |
| onClick={() => setSidebarOpen(!sidebarOpen)} | |
| className="lg:hidden fixed top-4 left-4 z-50 p-2 bg-white rounded-xl shadow-lg border border-slate-200" | |
| > | |
| {sidebarOpen ? <X className="h-5 w-5" /> : <Menu className="h-5 w-5" />} | |
| </button> | |
| {/* Sidebar */} | |
| <AnimatePresence> | |
| {(sidebarOpen || typeof window !== 'undefined' && window.innerWidth >= 1024) && ( | |
| <motion.aside | |
| initial={{ x: -300, opacity: 0 }} | |
| animate={{ x: 0, opacity: 1 }} | |
| exit={{ x: -300, opacity: 0 }} | |
| className={`fixed lg:sticky top-0 left-0 h-screen w-72 bg-white border-r border-slate-200 z-40 flex flex-col ${sidebarOpen ? 'block' : 'hidden lg:flex'}`} | |
| > | |
| <div className="p-6 border-b border-slate-100"> | |
| <div className="flex items-center gap-3"> | |
| <div className="p-2 bg-slate-900 rounded-xl"> | |
| <BookOpen className="h-5 w-5 text-white" /> | |
| </div> | |
| <div> | |
| <h1 className="font-bold text-lg text-slate-900">Knowledge Base</h1> | |
| <p className="text-xs text-slate-500">{articles.length} articles</p> | |
| </div> | |
| </div> | |
| </div> | |
| <div className="p-4 border-b border-slate-100"> | |
| <Button | |
| onClick={() => setSearchOpen(true)} | |
| variant="outline" | |
| className="w-full justify-start gap-2 h-11 rounded-xl border-slate-200 text-slate-500 hover:text-slate-700" | |
| > | |
| <Search className="h-4 w-4" /> | |
| Quick search... | |
| <kbd className="ml-auto text-xs bg-slate-100 px-1.5 py-0.5 rounded">⌘K</kbd> | |
| </Button> | |
| </div> | |
| <div className="flex-1 overflow-y-auto p-4"> | |
| <div className="flex items-center justify-between mb-3"> | |
| <h3 className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Categories</h3> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onClick={() => setCategoryModalOpen(true)} | |
| className="h-7 w-7 p-0 text-slate-400 hover:text-slate-600" | |
| > | |
| <FolderPlus className="h-4 w-4" /> | |
| </Button> | |
| </div> | |
| <CategoryTree | |
| categories={categories} | |
| articles={articles} | |
| /> | |
| </div> | |
| <div className="p-4 border-t border-slate-100"> | |
| <Button | |
| onClick={() => setEditorOpen(true)} | |
| className="w-full gap-2 h-11 rounded-xl bg-slate-900 hover:bg-slate-800" | |
| > | |
| <Plus className="h-4 w-4" /> | |
| New Article | |
| </Button> | |
| </div> | |
| </motion.aside> | |
| )} | |
| </AnimatePresence> | |
| {/* Mobile Overlay */} | |
| {sidebarOpen && ( | |
| <div | |
| className="fixed inset-0 bg-black/50 z-30 lg:hidden" | |
| onClick={() => setSidebarOpen(false)} | |
| /> | |
| )} | |
| {/* Main Content */} | |
| <main className="lg:ml-72 min-h-screen"> | |
| <div className="max-w-5xl mx-auto px-6 py-12"> | |
| {/* Hero Section */} | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| className="text-center mb-16" | |
| > | |
| <div className="inline-flex items-center gap-2 px-4 py-2 bg-amber-50 rounded-full text-amber-700 text-sm font-medium mb-6"> | |
| <Sparkles className="h-4 w-4" /> | |
| Team Knowledge Hub | |
| </div> | |
| <h1 className="text-4xl md:text-5xl font-bold text-slate-900 mb-4 tracking-tight"> | |
| Find answers, share knowledge | |
| </h1> | |
| <p className="text-lg text-slate-500 max-w-2xl mx-auto"> | |
| Your team's central repository for documentation, guides, and best practices | |
| </p> | |
| {/* Search Bar */} | |
| <div className="mt-10 max-w-2xl mx-auto"> | |
| <button | |
| onClick={() => setSearchOpen(true)} | |
| className="w-full flex items-center gap-3 px-6 py-4 bg-white rounded-2xl border-2 border-slate-200 hover:border-slate-300 shadow-sm hover:shadow-md transition-all text-left group" | |
| > | |
| <Search className="h-5 w-5 text-slate-400 group-hover:text-slate-600" /> | |
| <span className="text-slate-400 group-hover:text-slate-500">Search articles, code, docs...</span> | |
| <kbd className="ml-auto text-xs bg-slate-100 px-2 py-1 rounded-lg text-slate-500">⌘K</kbd> | |
| </button> | |
| </div> | |
| </motion.div> | |
| {/* Content Grid */} | |
| <div className="grid md:grid-cols-2 gap-8"> | |
| {/* Recent Articles */} | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.1 }} | |
| > | |
| <h2 className="text-lg font-semibold text-slate-900 mb-4 flex items-center gap-2"> | |
| <div className="p-1.5 bg-blue-100 rounded-lg"> | |
| <FileText className="h-4 w-4 text-blue-600" /> | |
| </div> | |
| Recently Updated | |
| </h2> | |
| <div className="space-y-2"> | |
| {recentArticles.map(article => ( | |
| <Link | |
| key={article.id} | |
| to={createPageUrl('Article') + `?id=${article.id}`} | |
| className="flex items-center gap-3 p-4 bg-white rounded-xl border border-slate-100 hover:border-slate-200 hover:shadow-sm transition-all group" | |
| > | |
| <div className="p-2 bg-slate-100 rounded-lg group-hover:bg-slate-200 transition-colors"> | |
| <FileText className="h-4 w-4 text-slate-600" /> | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <h4 className="font-medium text-slate-900 truncate">{article.title}</h4> | |
| <p className="text-sm text-slate-500 truncate">{article.summary}</p> | |
| </div> | |
| </Link> | |
| ))} | |
| {recentArticles.length === 0 && ( | |
| <div className="text-center py-8 text-slate-400"> | |
| No articles yet | |
| </div> | |
| )} | |
| </div> | |
| </motion.div> | |
| {/* Popular Articles */} | |
| <motion.div | |
| initial={{ opacity: 0, y: 20 }} | |
| animate={{ opacity: 1, y: 0 }} | |
| transition={{ delay: 0.2 }} | |
| > | |
| <h2 className="text-lg font-semibold text-slate-900 mb-4 flex items-center gap-2"> | |
| <div className="p-1.5 bg-amber-100 rounded-lg"> | |
| <Sparkles className="h-4 w-4 text-amber-600" /> | |
| </div> | |
| Most Helpful | |
| </h2> | |
| <div className="space-y-2"> | |
| {popularArticles.map(article => ( | |
| <Link | |
| key={article.id} | |
| to={createPageUrl('Article') + `?id=${article.id}`} | |
| className="flex items-center gap-3 p-4 bg-white rounded-xl border border-slate-100 hover:border-slate-200 hover:shadow-sm transition-all group" | |
| > | |
| <div className="p-2 bg-slate-100 rounded-lg group-hover:bg-slate-200 transition-colors"> | |
| <FileText className="h-4 w-4 text-slate-600" /> | |
| </div> | |
| <div className="flex-1 min-w-0"> | |
| <h4 className="font-medium text-slate-900 truncate">{article.title}</h4> | |
| <p className="text-sm text-slate-500 truncate">{article.summary}</p> | |
| </div> | |
| {article.helpful_count > 0 && ( | |
| <div className="text-xs text-emerald-600 bg-emerald-50 px-2 py-1 rounded-full"> | |
| 👍 {article.helpful_count} | |
| </div> | |
| )} | |
| </Link> | |
| ))} | |
| {popularArticles.length === 0 && ( | |
| <div className="text-center py-8 text-slate-400"> | |
| No articles yet | |
| </div> | |
| )} | |
| </div> | |
| </motion.div> | |
| </div> | |
| </div> | |
| </main> | |
| {/* Search Modal */} | |
| <Dialog open={searchOpen} onOpenChange={setSearchOpen}> | |
| <DialogContent className="sm:max-w-2xl p-0 gap-0 overflow-hidden"> | |
| <div className="p-6"> | |
| <SearchBar articles={articles} onClose={() => setSearchOpen(false)} /> | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| {/* Category Modal */} | |
| <Dialog open={categoryModalOpen} onOpenChange={setCategoryModalOpen}> | |
| <DialogContent className="sm:max-w-md"> | |
| <h2 className="text-lg font-semibold mb-4">Create Category</h2> | |
| <div className="space-y-4"> | |
| <div> | |
| <label className="text-sm font-medium text-slate-700">Name</label> | |
| <Input | |
| value={newCategoryName} | |
| onChange={(e) => setNewCategoryName(e.target.value)} | |
| placeholder="Category name" | |
| className="mt-1.5" | |
| /> | |
| </div> | |
| <div> | |
| <label className="text-sm font-medium text-slate-700">Parent (optional)</label> | |
| <select | |
| value={selectedParentId} | |
| onChange={(e) => setSelectedParentId(e.target.value)} | |
| className="w-full mt-1.5 h-10 rounded-md border border-slate-200 px-3" | |
| > | |
| <option value="">No parent (root level)</option> | |
| {categories.map(cat => ( | |
| <option key={cat.id} value={cat.id}>{cat.name}</option> | |
| ))} | |
| </select> | |
| </div> | |
| <div className="flex justify-end gap-2 pt-2"> | |
| <Button variant="ghost" onClick={() => setCategoryModalOpen(false)}> | |
| Cancel | |
| </Button> | |
| <Button onClick={handleCreateCategory} className="bg-slate-900 hover:bg-slate-800"> | |
| Create | |
| </Button> | |
| </div> | |
| </div> | |
| </DialogContent> | |
| </Dialog> | |
| {/* Article Editor */} | |
| <ArticleEditor | |
| categories={categories} | |
| open={editorOpen} | |
| onClose={() => setEditorOpen(false)} | |
| onSave={handleSaveArticle} | |
| /> | |
| </div> | |
| ); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment