Skip to content

Knowledge Base Frontend Implementation Guide

This guide provides step-by-step instructions to complete the knowledge base frontend implementation for the Producer Portal.

✅ What's Already Complete

  1. TypeScript Types - src/types/knowledge.ts
  2. All interfaces for Category, Tag, Article, Feedback
  3. Properly exported in src/types/index.ts

  4. API Client Functions - src/lib/api.ts

  5. fetchKnowledgeCategories()
  6. fetchKnowledgeArticle(slug)
  7. searchKnowledgeArticles(query)
  8. submitArticleFeedback(slug, feedback)
  9. Plus 12 more helper functions

🚀 Next Steps: Pages & Components

Directory Structure to Create

apps/producer/src/
├── app/dashboard/knowledge/
│   ├── page.tsx                           # ⬅️ HOME PAGE
│   ├── category/
│   │   └── [slug]/
│   │       └── page.tsx                   # ⬅️ CATEGORY PAGE
│   ├── article/
│   │   └── [slug]/
│   │       └── page.tsx                   # ⬅️ ARTICLE DETAIL PAGE
│   └── search/
│       └── page.tsx                       # ⬅️ SEARCH RESULTS PAGE
└── components/knowledge/
    ├── ArticleCard.tsx                    # ⬅️ ARTICLE PREVIEW CARD
    ├── CategoryCard.tsx                   # ⬅️ CATEGORY CARD WITH ICON
    ├── FeedbackWidget.tsx                 # ⬅️ HELPFUL/NOT HELPFUL WIDGET
    ├── SearchBar.tsx                      # ⬅️ SEARCH INPUT
    └── RelatedArticles.tsx                # ⬅️ RELATED ARTICLES SIDEBAR

📄 Page 1: Knowledge Base Home

File: apps/producer/src/app/dashboard/knowledge/page.tsx

import { Suspense } from 'react';
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { redirect } from 'next/navigation';
import {
  fetchKnowledgeCategories,
  fetchFeaturedArticles,
  fetchRecentArticles
} from '@/lib/api';
import type { ExtendedSession } from '@/lib/api';
import CategoryCard from '@/components/knowledge/CategoryCard';
import ArticleCard from '@/components/knowledge/ArticleCard';
import SearchBar from '@/components/knowledge/SearchBar';

export const metadata = {
  title: 'Knowledge Base | PiqueTickets Producer Portal',
  description: 'Find answers and learn how to use the Producer Portal',
};

export default async function KnowledgeBasePage() {
  const session = (await getServerSession(authOptions)) as ExtendedSession;

  if (!session) {
    redirect('/login');
  }

  // Fetch data in parallel
  const [categoriesRes, featuredRes, recentRes] = await Promise.all([
    fetchKnowledgeCategories(session),
    fetchFeaturedArticles(session),
    fetchRecentArticles(session),
  ]);

  const categories = categoriesRes.data || [];
  const featured = featuredRes.data || [];
  const recent = recentRes.data || [];

  return (
    <div className="container mx-auto px-4 py-8 max-w-7xl">
      {/* Header */}
      <div className="mb-8">
        <h1 className="text-4xl font-bold mb-2">Knowledge Base</h1>
        <p className="text-muted-foreground text-lg">
          Everything you need to know about using PiqueTickets
        </p>
      </div>

      {/* Search Bar */}
      <div className="mb-12">
        <SearchBar />
      </div>

      {/* Categories Grid */}
      <section className="mb-12">
        <h2 className="text-2xl font-semibold mb-6">Browse by Category</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {categories.map((category) => (
            <CategoryCard key={category.id} category={category} />
          ))}
        </div>
      </section>

      {/* Featured Articles */}
      {featured.length > 0 && (
        <section className="mb-12">
          <h2 className="text-2xl font-semibold mb-6">Featured Articles</h2>
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            {featured.slice(0, 3).map((article) => (
              <ArticleCard key={article.id} article={article} />
            ))}
          </div>
        </section>
      )}

      {/* Recent Articles */}
      <section>
        <h2 className="text-2xl font-semibold mb-6">Recent Articles</h2>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {recent.slice(0, 6).map((article) => (
            <ArticleCard key={article.id} article={article} />
          ))}
        </div>
      </section>
    </div>
  );
}

📄 Page 2: Category View

File: apps/producer/src/app/dashboard/knowledge/category/[slug]/page.tsx

import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { redirect } from 'next/navigation';
import { fetchKnowledgeCategoryBySlug, fetchCategoryArticles } from '@/lib/api';
import type { ExtendedSession } from '@/lib/api';
import ArticleCard from '@/components/knowledge/ArticleCard';
import { ArrowLeft } from 'lucide-react';
import Link from 'next/link';

export async function generateMetadata({ params }: { params: { slug: string } }) {
  return {
    title: `${params.slug} | Knowledge Base`,
  };
}

export default async function CategoryPage({ params }: { params: { slug: string } }) {
  const session = (await getServerSession(authOptions)) as ExtendedSession;

  if (!session) {
    redirect('/login');
  }

  const [categoryRes, articlesRes] = await Promise.all([
    fetchKnowledgeCategoryBySlug(params.slug, session),
    fetchCategoryArticles(params.slug, session),
  ]);

  if (categoryRes.error || !categoryRes.data) {
    redirect('/dashboard/knowledge');
  }

  const category = categoryRes.data;
  const articles = articlesRes.data || [];

  return (
    <div className="container mx-auto px-4 py-8 max-w-7xl">
      {/* Breadcrumb */}
      <nav className="mb-6">
        <Link
          href="/dashboard/knowledge"
          className="inline-flex items-center text-sm text-muted-foreground hover:text-foreground"
        >
          <ArrowLeft className="mr-2 h-4 w-4" />
          Back to Knowledge Base
        </Link>
      </nav>

      {/* Category Header */}
      <div className="mb-8">
        <div className="flex items-center gap-4 mb-4">
          {category.icon && (
            <span className="material-icons text-4xl">{category.icon}</span>
          )}
          <h1 className="text-4xl font-bold">{category.name}</h1>
        </div>
        {category.description && (
          <p className="text-muted-foreground text-lg">{category.description}</p>
        )}
        <p className="text-sm text-muted-foreground mt-2">
          {category.article_count} {category.article_count === 1 ? 'article' : 'articles'}
        </p>
      </div>

      {/* Subcategories */}
      {category.subcategories && category.subcategories.length > 0 && (
        <section className="mb-8">
          <h2 className="text-xl font-semibold mb-4">Subcategories</h2>
          <div className="flex flex-wrap gap-2">
            {category.subcategories.map((subcat) => (
              <Link
                key={subcat.id}
                href={`/dashboard/knowledge/category/${subcat.slug}`}
                className="px-4 py-2 bg-secondary text-secondary-foreground rounded-md hover:bg-secondary/80 transition-colors"
              >
                {subcat.name}
              </Link>
            ))}
          </div>
        </section>
      )}

      {/* Articles */}
      <section>
        {articles.length > 0 ? (
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
            {articles.map((article) => (
              <ArticleCard key={article.id} article={article} />
            ))}
          </div>
        ) : (
          <div className="text-center py-12">
            <p className="text-muted-foreground">No articles in this category yet.</p>
          </div>
        )}
      </section>
    </div>
  );
}

📄 Page 3: Article Detail

File: apps/producer/src/app/dashboard/knowledge/article/[slug]/page.tsx

import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { redirect } from 'next/navigation';
import { fetchKnowledgeArticle, fetchRelatedArticles } from '@/lib/api';
import type { ExtendedSession } from '@/lib/api';
import FeedbackWidget from '@/components/knowledge/FeedbackWidget';
import RelatedArticles from '@/components/knowledge/RelatedArticles';
import { Clock, Calendar, Tag } from 'lucide-react';
import Link from 'next/link';
import Image from 'next/image';

export async function generateMetadata({ params }: { params: { slug: string } }) {
  return {
    title: `${params.slug} | Knowledge Base`,
  };
}

export default async function ArticlePage({ params }: { params: { slug: string } }) {
  const session = (await getServerSession(authOptions)) as ExtendedSession;

  if (!session) {
    redirect('/login');
  }

  const [articleRes, relatedRes] = await Promise.all([
    fetchKnowledgeArticle(params.slug, session),
    fetchRelatedArticles(params.slug, session),
  ]);

  if (articleRes.error || !articleRes.data) {
    redirect('/dashboard/knowledge');
  }

  const article = articleRes.data;
  const related = relatedRes.data || [];

  return (
    <div className="container mx-auto px-4 py-8 max-w-7xl">
      <div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
        {/* Main Content */}
        <div className="lg:col-span-2">
          {/* Breadcrumb */}
          <nav className="mb-6 text-sm">
            <Link href="/dashboard/knowledge" className="text-muted-foreground hover:text-foreground">
              Knowledge Base
            </Link>
            <span className="mx-2">/</span>
            <Link
              href={`/dashboard/knowledge/category/${article.category.slug}`}
              className="text-muted-foreground hover:text-foreground"
            >
              {article.category.name}
            </Link>
          </nav>

          {/* Article Header */}
          <article>
            <h1 className="text-4xl font-bold mb-4">{article.title}</h1>

            {/* Meta Info */}
            <div className="flex flex-wrap gap-4 text-sm text-muted-foreground mb-6">
              <div className="flex items-center gap-1">
                <Clock className="h-4 w-4" />
                {article.estimated_read_time} min read
              </div>
              <div className="flex items-center gap-1">
                <Calendar className="h-4 w-4" />
                {new Date(article.published_at || '').toLocaleDateString()}
              </div>
              <div className="flex items-center gap-1">
                👁️ {article.view_count} views
              </div>
            </div>

            {/* Featured Image */}
            {article.featured_image && (
              <div className="mb-8 rounded-lg overflow-hidden">
                <Image
                  src={article.featured_image}
                  alt={article.featured_image_alt_text || article.title}
                  width={1200}
                  height={600}
                  className="w-full h-auto"
                />
              </div>
            )}

            {/* Article Content */}
            <div
              className="prose prose-lg max-w-none mb-8"
              dangerouslySetInnerHTML={{ __html: article.content }}
            />

            {/* Tags */}
            {article.tags && article.tags.length > 0 && (
              <div className="mb-8">
                <div className="flex items-center gap-2 mb-3">
                  <Tag className="h-4 w-4" />
                  <span className="font-semibold">Tags:</span>
                </div>
                <div className="flex flex-wrap gap-2">
                  {article.tags.map((tag) => (
                    <Link
                      key={tag.id}
                      href={`/dashboard/knowledge?tag=${tag.slug}`}
                      className="px-3 py-1 bg-secondary text-secondary-foreground rounded-full text-sm hover:bg-secondary/80"
                    >
                      {tag.name}
                    </Link>
                  ))}
                </div>
              </div>
            )}

            {/* Feedback Widget */}
            <FeedbackWidget article={article} />
          </article>
        </div>

        {/* Sidebar */}
        <aside className="lg:col-span-1">
          <RelatedArticles articles={related} />
        </aside>
      </div>
    </div>
  );
}

📄 Page 4: Search Results

File: apps/producer/src/app/dashboard/knowledge/search/page.tsx

import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import { redirect } from 'next/navigation';
import { searchKnowledgeArticles } from '@/lib/api';
import type { ExtendedSession } from '@/lib/api';
import ArticleCard from '@/components/knowledge/ArticleCard';
import SearchBar from '@/components/knowledge/SearchBar';
import Link from 'next/link';

export default async function SearchPage({
  searchParams,
}: {
  searchParams: { q?: string; category?: string; tag?: string };
}) {
  const session = (await getServerSession(authOptions)) as ExtendedSession;

  if (!session) {
    redirect('/login');
  }

  const query = searchParams.q || '';

  if (!query) {
    redirect('/dashboard/knowledge');
  }

  const searchRes = await searchKnowledgeArticles(
    query,
    session,
    searchParams.category,
    searchParams.tag
  );

  const results = searchRes.data?.results || [];
  const count = searchRes.data?.count || 0;

  return (
    <div className="container mx-auto px-4 py-8 max-w-7xl">
      {/* Header */}
      <div className="mb-8">
        <h1 className="text-4xl font-bold mb-2">Search Results</h1>
        <p className="text-muted-foreground">
          Found {count} {count === 1 ? 'result' : 'results'} for "{query}"
        </p>
      </div>

      {/* Search Bar */}
      <div className="mb-8">
        <SearchBar initialQuery={query} />
      </div>

      {/* Results */}
      {results.length > 0 ? (
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {results.map((article) => (
            <ArticleCard key={article.id} article={article} highlightQuery={query} />
          ))}
        </div>
      ) : (
        <div className="text-center py-12">
          <p className="text-muted-foreground text-lg mb-4">
            No articles found matching your search.
          </p>
          <Link
            href="/dashboard/knowledge"
            className="text-primary hover:underline"
          >
            Browse all categories
          </Link>
        </div>
      )}
    </div>
  );
}

🧩 Component 1: ArticleCard

File: apps/producer/src/components/knowledge/ArticleCard.tsx

'use client';

import Link from 'next/link';
import Image from 'next/image';
import { Clock, ThumbsUp } from 'lucide-react';
import type { ArticleListItem } from '@/types/knowledge';
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';

interface ArticleCardProps {
  article: ArticleListItem;
  highlightQuery?: string;
}

export default function ArticleCard({ article, highlightQuery }: ArticleCardProps) {
  return (
    <Link href={`/dashboard/knowledge/article/${article.slug}`}>
      <Card className="h-full hover:shadow-lg transition-shadow cursor-pointer">
        {article.featured_image && (
          <div className="relative h-48 w-full overflow-hidden rounded-t-lg">
            <Image
              src={article.featured_image}
              alt={article.featured_image_alt_text || article.title}
              fill
              className="object-cover"
            />
          </div>
        )}

        <CardHeader>
          <div className="flex items-start justify-between gap-2 mb-2">
            <Badge variant="outline">{article.category_name}</Badge>
            {article.featured && <Badge>Featured</Badge>}
          </div>
          <h3 className="font-semibold text-lg line-clamp-2">{article.title}</h3>
        </CardHeader>

        <CardContent>
          <p className="text-muted-foreground text-sm line-clamp-3">
            {article.excerpt}
          </p>
        </CardContent>

        <CardFooter className="flex items-center justify-between text-sm text-muted-foreground">
          <div className="flex items-center gap-1">
            <Clock className="h-3 w-3" />
            {article.estimated_read_time} min
          </div>
          {article.helpfulness_percentage !== null && (
            <div className="flex items-center gap-1">
              <ThumbsUp className="h-3 w-3" />
              {article.helpfulness_percentage}%
            </div>
          )}
        </CardFooter>
      </Card>
    </Link>
  );
}

🧩 Component 2: CategoryCard

File: apps/producer/src/components/knowledge/CategoryCard.tsx

'use client';

import Link from 'next/link';
import type { Category } from '@/types/knowledge';
import { Card, CardContent, CardHeader } from '@/components/ui/card';

interface CategoryCardProps {
  category: Category;
}

export default function CategoryCard({ category }: CategoryCardProps) {
  return (
    <Link href={`/dashboard/knowledge/category/${category.slug}`}>
      <Card className="h-full hover:shadow-lg transition-shadow cursor-pointer">
        <CardHeader>
          <div className="flex items-center gap-3">
            {category.icon && (
              <span className="material-icons text-3xl text-primary">
                {category.icon}
              </span>
            )}
            <div>
              <h3 className="font-semibold text-lg">{category.name}</h3>
              <p className="text-sm text-muted-foreground">
                {category.article_count} {category.article_count === 1 ? 'article' : 'articles'}
              </p>
            </div>
          </div>
        </CardHeader>

        {category.description && (
          <CardContent>
            <p className="text-sm text-muted-foreground line-clamp-2">
              {category.description}
            </p>
          </CardContent>
        )}
      </Card>
    </Link>
  );
}

🧩 Component 3: FeedbackWidget

File: apps/producer/src/components/knowledge/FeedbackWidget.tsx

'use client';

import { useState } from 'react';
import { ThumbsUp, ThumbsDown } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { submitArticleFeedback } from '@/lib/api';
import { useSession } from 'next-auth/react';
import type { Article } from '@/types/knowledge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';

interface FeedbackWidgetProps {
  article: Article;
}

export default function FeedbackWidget({ article }: FeedbackWidgetProps) {
  const { data: session } = useSession();
  const [submitted, setSubmitted] = useState(!!article.user_feedback);
  const [isHelpful, setIsHelpful] = useState<boolean | null>(
    article.user_feedback?.is_helpful ?? null
  );
  const [comment, setComment] = useState('');
  const [showComment, setShowComment] = useState(false);
  const [loading, setLoading] = useState(false);

  const handleFeedback = async (helpful: boolean) => {
    if (submitted || !session) return;

    setLoading(true);
    setIsHelpful(helpful);

    if (!helpful) {
      setShowComment(true);
      setLoading(false);
      return;
    }

    const result = await submitArticleFeedback(
      article.slug,
      { is_helpful: helpful },
      session as any
    );

    setLoading(false);

    if (result.data) {
      setSubmitted(true);
    }
  };

  const handleSubmitWithComment = async () => {
    if (!session) return;

    setLoading(true);

    const result = await submitArticleFeedback(
      article.slug,
      { is_helpful: false, comment },
      session as any
    );

    setLoading(false);

    if (result.data) {
      setSubmitted(true);
      setShowComment(false);
    }
  };

  if (submitted) {
    return (
      <Card className="bg-green-50 border-green-200">
        <CardContent className="pt-6">
          <p className="text-green-800 text-center">
            Thank you for your feedback! 🎉
          </p>
        </CardContent>
      </Card>
    );
  }

  return (
    <Card>
      <CardHeader>
        <CardTitle className="text-lg">Was this article helpful?</CardTitle>
      </CardHeader>
      <CardContent>
        {!showComment ? (
          <div className="flex gap-4">
            <Button
              onClick={() => handleFeedback(true)}
              disabled={loading}
              variant="outline"
              className="flex-1"
            >
              <ThumbsUp className="mr-2 h-4 w-4" />
              Yes, helpful
            </Button>
            <Button
              onClick={() => handleFeedback(false)}
              disabled={loading}
              variant="outline"
              className="flex-1"
            >
              <ThumbsDown className="mr-2 h-4 w-4" />
              No, not helpful
            </Button>
          </div>
        ) : (
          <div className="space-y-4">
            <p className="text-sm text-muted-foreground">
              We're sorry this wasn't helpful. Can you tell us why?
            </p>
            <Textarea
              value={comment}
              onChange={(e) => setComment(e.target.value)}
              placeholder="Your feedback helps us improve..."
              rows={3}
            />
            <div className="flex gap-2">
              <Button onClick={handleSubmitWithComment} disabled={loading}>
                Submit Feedback
              </Button>
              <Button
                variant="ghost"
                onClick={() => {
                  setShowComment(false);
                  setIsHelpful(null);
                }}
              >
                Cancel
              </Button>
            </div>
          </div>
        )}
      </CardContent>
    </Card>
  );
}

File: apps/producer/src/components/knowledge/SearchBar.tsx

'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { Search } from 'lucide-react';
import { Input } from '@/components/ui/input';
import { Button } from '@/components/ui/button';

interface SearchBarProps {
  initialQuery?: string;
}

export default function SearchBar({ initialQuery = '' }: SearchBarProps) {
  const router = useRouter();
  const [query, setQuery] = useState(initialQuery);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    if (query.trim()) {
      router.push(`/dashboard/knowledge/search?q=${encodeURIComponent(query)}`);
    }
  };

  return (
    <form onSubmit={handleSubmit} className="flex gap-2 max-w-2xl">
      <div className="relative flex-1">
        <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
        <Input
          type="search"
          placeholder="Search articles..."
          value={query}
          onChange={(e) => setQuery(e.target.value)}
          className="pl-10"
        />
      </div>
      <Button type="submit">Search</Button>
    </form>
  );
}

File: apps/producer/src/components/knowledge/RelatedArticles.tsx

import Link from 'next/link';
import type { ArticleListItem } from '@/types/knowledge';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Clock } from 'lucide-react';

interface RelatedArticlesProps {
  articles: ArticleListItem[];
}

export default function RelatedArticles({ articles }: RelatedArticlesProps) {
  if (articles.length === 0) {
    return null;
  }

  return (
    <Card>
      <CardHeader>
        <CardTitle>Related Articles</CardTitle>
      </CardHeader>
      <CardContent>
        <div className="space-y-4">
          {articles.map((article) => (
            <Link
              key={article.id}
              href={`/dashboard/knowledge/article/${article.slug}`}
              className="block group"
            >
              <h4 className="font-medium group-hover:text-primary transition-colors mb-1">
                {article.title}
              </h4>
              <div className="flex items-center gap-2 text-sm text-muted-foreground">
                <span>{article.category_name}</span>
                <span></span>
                <div className="flex items-center gap-1">
                  <Clock className="h-3 w-3" />
                  {article.estimated_read_time} min
                </div>
              </div>
            </Link>
          ))}
        </div>
      </CardContent>
    </Card>
  );
}

🔧 Final Step: Update Navigation

Update the sidebar to include the Knowledge Base link.

Find the sidebar navigation file (likely apps/producer/src/components/layout/Sidebar.tsx or similar) and add:

{
  title: 'Knowledge Base',
  url: '/dashboard/knowledge',
  icon: 'BookOpen', // or whatever icon set you're using
  description: 'Help & Documentation',
}

🚦 Feature Flag Integration

The Knowledge Base is protected by the knowledgeBaseEnabled producer-level feature flag. This integration was completed as part of PIQUE-506.

How It Works

  1. Navigation Display
  2. The Knowledge Base navigation item only appears when knowledgeBaseEnabled is true
  3. Configured in apps/producer/src/constants/data.ts with featureFlag: 'knowledgeBaseEnabled'
  4. The sidebar automatically filters feature-flagged items using useFeatureFlags() hook

  5. Route Protection

  6. All routes under /dashboard/knowledge/* are protected by a layout component
  7. Located at: apps/producer/src/app/dashboard/knowledge/layout.tsx
  8. Performs server-side check of feature flag before rendering any pages
  9. Redirects to /dashboard/overview if feature is disabled

  10. Implementation Details

    // apps/producer/src/app/dashboard/knowledge/layout.tsx
    // - Checks feature flag on server-side
    // - Redirects if not enabled
    // - Handles errors gracefully
    

Enabling/Disabling the Feature

Via Django Admin: 1. Navigate to /admin/ 2. Go to "Producer Profiles" 3. Find the producer profile 4. Check/uncheck "Knowledge Base Enabled" 5. Save changes

Effect: - When enabled: Nav item appears, all routes accessible - When disabled: Nav item hidden, direct URL access redirected - Changes take effect after page refresh or cache expiration (5 minutes)

Testing Feature Flags

Test Scenarios: 1. ✅ Enable flag → Nav item appears 2. ✅ Enable flag → Can access /dashboard/knowledge 3. ✅ Disable flag → Nav item disappears 4. ✅ Disable flag → Direct URL access redirects to dashboard 5. ✅ Disable flag → All sub-routes redirect (category, article, search)

Related Files: - apps/producer/src/constants/data.ts - Navigation configuration - apps/producer/src/app/dashboard/knowledge/layout.tsx - Route protection - apps/producer/src/hooks/use-feature-flags.ts - Feature flag hook - apps/producer/src/components/layout/app-sidebar.tsx - Sidebar filtering - apps/producer/src/types/index.ts - FeatureFlags interface


🎨 Styling Notes

The components use Shadcn UI components (Card, Button, Input, etc.) that should already be installed in your project. If any are missing, install them with:

npx shadcn-ui@latest add card button input textarea badge

✅ Testing Checklist

Once implemented, test:

  1. Home Page - Categories, featured articles, search bar all display
  2. Category Page - Articles filter correctly, breadcrumbs work
  3. Article Detail - Content renders, images display, feedback works
  4. Search - Returns relevant results, filters work
  5. Navigation - All links work correctly
  6. Mobile - Responsive on all screen sizes

🚀 Next Steps After Implementation

  1. Run migrations on the backend (when Docker is available)
  2. Create sample categories and articles via Django admin
  3. Test the complete flow from frontend to backend
  4. Add Material Icons if using icon-based categories
  5. Consider adding pagination for long article lists

Questions or Issues?

  • Check the backend README: apps/api/knowledge/README.md
  • Review API responses in browser DevTools
  • Ensure JWT auth is working correctly

Good luck with the implementation! 🎉