Skip to content

Instantly share code, notes, and snippets.

@paulweezydesign
Last active January 2, 2026 01:23
Show Gist options
  • Select an option

  • Save paulweezydesign/b2d4dbe20d0621cc0a70410c74120224 to your computer and use it in GitHub Desktop.

Select an option

Save paulweezydesign/b2d4dbe20d0621cc0a70410c74120224 to your computer and use it in GitHub Desktop.
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;
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>
);
}
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