features - blueprint-site/blueprint-create GitHub Wiki

Addons Feature

The Addons feature is a core part of Blueprint, allowing users to discover, browse, and filter Create Mod addons.

Overview

The Addons feature provides a comprehensive browsing experience for Create Mod addons, including filtering by version, category, and mod loader compatibility. It integrates data from multiple sources like CurseForge and Modrinth, providing a unified interface for addon discovery.

User Flows

Browsing Addons

  1. User navigates to the Addons page (/addons)
  2. User can:
    • Search for addons by name
    • Filter by Minecraft version
    • Filter by Create Mod version
    • Filter by mod loader (Forge, Fabric, NeoForge)
    • Filter by category
  3. Results update in real-time as filters are applied
  4. Clicking on an addon navigates to its details page

Viewing Addon Details

  1. User navigates to an addon details page (/addons/:slug)
  2. User sees addon details:
    • Name and description
    • Screenshots gallery
    • Downloads count and statistics
    • Author information
    • Compatible Minecraft versions
    • Compatible Create Mod versions
    • Supported mod loaders
    • External links (website, source, issues)
  3. User can download the addon from the original source
  4. User can add the addon to a personal collection

Adding to Collection

  1. User clicks "Add to Collection" button on an addon card or details page
  2. If not logged in, user is prompted to log in
  3. User selects an existing collection or creates a new one
  4. Addon is added to the collection
  5. Confirmation is shown to the user

Components

Key Components

AddonList

  • Main component for displaying the list of addons
  • Located at /src/components/features/addons/AddonList.tsx
  • Responsibilities:
    • Displaying a grid of addon cards
    • Handling pagination
    • Managing loading states
    • Empty state handling

AddonCard

  • Card component for individual addon display in lists
  • Located at /src/components/features/addons/addon-card/AddonCard.tsx
  • Subcomponents:
    • AddonStats.tsx - Downloads and other metrics
    • CategoryBadges.tsx - Visual indicators for categories
    • ModLoaders.tsx - Icons for supported mod loaders
    • VersionBadges.tsx - Compatible versions display
    • ExternalLinks.tsx - Links to external resources

AddonDetails

  • Detailed view of an addon
  • Located at /src/components/features/addons/addon-details/AddonDetailsContent.tsx
  • Subcomponents:
    • AddonDetailsHeader.tsx - Title, author, and key stats
    • AddonDetailsGallery.tsx - Screenshot gallery
    • AddonDetailsDescription.tsx - Markdown description
    • AddonDetailsFooter.tsx - Actions and additional info

FiltersContainer

  • Container for all filter components
  • Located at /src/components/layout/FiltersContainer.tsx
  • Used by:
    • SearchFilter.tsx - Search input
    • SelectFilter.tsx - Dropdown filters for version and categories

State Management

Addon state is managed through custom hooks:

// Example usage
const AddonListPage = () => {
  const { filters, setFilter, resetFilters } = useFilters();
  const { data, isLoading, error } = useSearchAddons(filters);
  
  // Component implementation
}

useAddons

  • Located at /src/api/endpoints/useAddons.tsx
  • Purpose: Fetches and manages addon data from Appwrite
  • Key functions:
    • getAddon: Get a single addon by ID or slug
    • getAddons: Get multiple addons with pagination

useSearchAddons

  • Located at /src/api/endpoints/useSearchAddons.tsx
  • Purpose: Handles search and filtering using Meilisearch
  • Key parameters:
    • query: Search text
    • category: Filter by category
    • version: Filter by Minecraft version
    • createVersion: Filter by Create Mod version
    • loaders: Filter by mod loader

useFilters

  • Located at /src/hooks/useFilters.ts
  • Purpose: Manages filter state and URL query params
  • Key functions:
    • setFilter: Update a specific filter
    • resetFilters: Clear all filters
    • getFilterValues: Get current filter values

Data Model

Addons follow the AddonSchema defined in /src/schemas/addon.schema.tsx:

export const AddonSchema = z.object({
  $id: z.string(),
  name: z.string(),
  description: z.string(),
  slug: z.string(),
  author: z.string(),
  categories: z.array(z.string()),
  downloads: z.number(),
  icon: z.string(),
  created_at: z.string().nullable(),
  updated_at: z.string().nullable(),
  curseforge_raw: CurseForgeAddonSchema.optional().nullable(),
  modrinth_raw: ModrinthAddonSchema.optional().nullable(),
  sources: z.array(z.string()),
  isValid: z.boolean(),
  loaders: z.array(z.string()).nullable(),
  isChecked: z.boolean(),
  minecraft_versions: z.array(z.string()).optional().nullable(),
  create_versions: z.array(z.string()).optional().nullable(),
});

Each addon contains:

  • Basic information (name, description, slug)
  • Author information
  • Category tags
  • Download statistics
  • Compatibility information (Minecraft versions, Create versions, mod loaders)
  • Raw data from original sources (CurseForge, Modrinth)
  • Validation flags

API Integration

Appwrite Integration

Addons are stored in the Appwrite database in the addons collection:

// Example of fetching an addon from Appwrite
const getAddon = async (slug: string) => {
  try {
    const response = await databases.listDocuments(
      DATABASE_ID,
      ADDONS_COLLECTION_ID,
      [
        Query.equal("slug", slug)
      ]
    );
    
    if (response.documents.length === 0) {
      throw new Error('Addon not found');
    }
    
    return response.documents[0];
  } catch (error) {
    console.error('Error fetching addon:', error);
    throw error;
  }
};

Meilisearch Integration

Addons are indexed in Meilisearch for fast search and filtering:

// Example of searching addons with Meilisearch
const searchAddons = async (
  query = '',
  filters = {},
  page = 1,
  limit = 20
) => {
  try {
    const index = searchClient.index('addons');
    const filterStr = buildFilterString(filters);
    
    return index.search(query, {
      filter: filterStr,
      limit,
      offset: (page - 1) * limit
    });
  } catch (error) {
    console.error('Error searching addons:', error);
    throw error;
  }
};

Implementation Details

Data Sources

Addon data is sourced from:

  1. CurseForge API

    • Provides comprehensive addon metadata
    • Includes download statistics
    • Offers detailed version compatibility
  2. Modrinth API

    • Alternative source for modding content
    • Different community focus
    • Additional compatibility information
  3. Manual Submissions

    • For addons not available on major platforms
    • Community-contributed information
    • Admin-verified data

Synchronization Process

Addon data is synchronized between Appwrite and Meilisearch:

  1. Data is written to or updated in Appwrite
  2. A background process detects changes
  3. Updated data is sent to Meilisearch
  4. Search index is updated with new information

There is approximately a 1-minute delay between updates in Appwrite and their availability in Meilisearch search results.

Caching Strategy

  • Meilisearch results: Cached for 5 minutes (staleTime in TanStack Query)
  • Appwrite detailed data: Cached for 10 minutes
  • Images and assets: Cached according to browser defaults
  • Invalidation: Triggered on mutations or manual refresh

Best Practices

Component Usage

Always use the existing component hierarchy:

// ✅ Good - Using the proper components
<AddonList>
  {addons.map(addon => (
    <AddonCard key={addon.$id} addon={addon} />
  ))}
</AddonList>

// ❌ Bad - Bypassing the component structure
<div className="grid">
  {addons.map(addon => (
    <div key={addon.$id} className="card">
      {/* Custom implementation */}
    </div>
  ))}
</div>

Data Validation

Always validate addon data against the schema:

// ✅ Good - Using schema validation
const parseAddon = (data: unknown) => {
  try {
    return AddonSchema.parse(data);
  } catch (error) {
    console.error('Invalid addon data:', error);
    return null;
  }
};

// ❌ Bad - Not validating data
const addon = data as Addon; // Unsafe type assertion

Search and Filter Implementation

Use the designated hooks for search and filter operations:

// ✅ Good - Using the search hooks
const { data, isLoading } = useSearchAddons(filters);

// ❌ Bad - Direct API calls
const [addons, setAddons] = useState([]);
useEffect(() => {
  const fetchData = async () => {
    const results = await searchClient.index('addons').search(query);
    setAddons(results.hits);
  };
  fetchData();
}, [query]);

Troubleshooting

Common Issues

No Addons Appearing

  • Check if Meilisearch is properly configured
  • Verify the index exists and contains data
  • Ensure filters aren't too restrictive
  • Check console for API errors

Addon Details Not Loading

  • Verify the slug is correct
  • Check if the addon exists in Appwrite
  • Ensure all required fields are present
  • Check for permissions issues

Filters Not Working

  • Verify filterable attributes in Meilisearch
  • Check the filter syntax
  • Ensure the filter values exist in the data
  • Look for JavaScript errors in the console

Images Not Loading

  • Check if image URLs are correct
  • Verify CORS settings on the image host
  • Check network tab for 404 errors
  • Ensure the storage bucket is accessible

Future Enhancements

Planned improvements for the Addons feature:

  1. Version Compatibility Matrix: Visual display of compatibility across versions
  2. Change Tracking: History of updates and changes to addons
  3. Dependency Visualization: Graph view of addon dependencies
  4. Community Ratings: User-provided ratings and reviews
  5. Collection Sharing: Public sharing of addon collections
  6. Mod Pack Integration: Creating mod packs from addon collections

Schematics Feature

The Schematics feature allows users to share, discover, and download Create Mod contraption designs. This document outlines the functionality, components, and implementation details of the Schematics feature.

Overview

The Schematics feature enables users to:

  • Upload Create Mod schematics with metadata
  • Search and filter schematics by various criteria
  • Preview schematic details and images
  • Download schematics for use in Minecraft
  • Rate and comment on community schematics

User Flows

Browsing Schematics

  1. User navigates to the Schematics page
  2. User can:
    • Search for schematics by name or description
    • Filter by Minecraft version
    • Filter by Create Mod version
    • Filter by tags/categories
    • Sort by popularity, date, or rating

Uploading a Schematic

  1. User clicks "Upload Schematic" button
  2. User is prompted to log in if not already authenticated
  3. User fills out the upload form:
    • Schematic name
    • Description
    • Tags/categories
    • Minecraft version
    • Create Mod version
    • Schematic file (.nbt or .schem)
    • Preview image(s)
  4. User submits the form
  5. System processes and validates the schematic
  6. Confirmation is shown to the user

Viewing Schematic Details

  1. User clicks on a schematic card
  2. User sees schematic details:
    • Name and description
    • Author information
    • Creation/update date
    • Minecraft and Create Mod version
    • Tags/categories
    • Preview images
    • Download count
    • Ratings and comments
  3. User can download the schematic file
  4. User can rate the schematic
  5. User can leave comments if authenticated

Managing User Schematics

  1. User navigates to their profile
  2. User selects "My Schematics" tab
  3. User can:
    • View all their uploaded schematics
    • Edit existing schematics
    • Delete their schematics
    • See download and rating statistics

Components

Key Components

Schematics List

The main component for displaying schematics with filtering and sorting options:

// src/components/features/schematics/SchematicsList.tsx
import { useState } from 'react';
import { useSearchSchematics } from '@/api/endpoints/useSearchSchematics';
import { SchematicCard } from './SchematicCard';
import { SchematicsFilters } from './SchematicsFilters';
import { Pagination } from '@/components/common/Pagination';

export const SchematicsList = () => {
  const [searchParams, setSearchParams] = useState({
    query: '',
    filters: '',
    page: 1,
    hitsPerPage: 20,
    sort: ['created_at:desc'],
  });

  const { data, isLoading, error } = useSearchSchematics(searchParams);

  // Component implementation
  return (
    <div className="container mx-auto">
      <SchematicsFilters 
        onFilterChange={(filters) => setSearchParams(prev => ({ ...prev, filters, page: 1 }))}
        onSearchChange={(query) => setSearchParams(prev => ({ ...prev, query, page: 1 }))}
        onSortChange={(sort) => setSearchParams(prev => ({ ...prev, sort, page: 1 }))}
      />
      
      {isLoading ? (
        <LoadingGrid />
      ) : error ? (
        <ErrorMessage error={error} />
      ) : (
        <>
          <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
            {data?.hits.map(schematic => (
              <SchematicCard key={schematic.$id} schematic={schematic} />
            ))}
          </div>
          
          <Pagination
            currentPage={data?.page || 1}
            totalPages={data?.totalPages || 1}
            onPageChange={(page) => setSearchParams(prev => ({ ...prev, page }))}
          />
        </>
      )}
    </div>
  );
};

Schematic Card

Card component for displaying schematic previews:

// src/components/features/schematics/SchematicCard.tsx
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card';
import { Schematic } from '@/types';
import { Link } from 'react-router-dom';
import { formatDate, formatNumber } from '@/lib/utils';

interface SchematicCardProps {
  schematic: Schematic;
}

export const SchematicCard: React.FC<SchematicCardProps> = ({ schematic }) => {
  return (
    <Link to={`/schematics/${schematic.$id}`}>
      <Card className="h-full hover:shadow-md transition-shadow">
        <CardHeader className="p-0">
          <div className="relative aspect-video w-full overflow-hidden">
            <img
              src={schematic.preview || '/placeholders/schematic.png'}
              alt={schematic.name}
              className="object-cover w-full h-full"
            />
            <div className="absolute bottom-0 left-0 right-0 bg-gradient-to-t from-black/60 to-transparent p-4">
              <h3 className="text-white font-bold text-lg line-clamp-1">{schematic.name}</h3>
            </div>
          </div>
        </CardHeader>
        <CardContent className="p-4">
          <p className="text-sm text-muted-foreground line-clamp-2">{schematic.description}</p>
          <div className="mt-2 flex flex-wrap gap-1">
            {schematic.tags.slice(0, 3).map(tag => (
              <span key={tag} className="text-xs bg-secondary px-2 py-1 rounded-full">
                {tag}
              </span>
            ))}
            {schematic.tags.length > 3 && (
              <span className="text-xs bg-secondary px-2 py-1 rounded-full">
                +{schematic.tags.length - 3}
              </span>
            )}
          </div>
        </CardContent>
        <CardFooter className="p-4 pt-0 text-xs text-muted-foreground">
          <div className="flex justify-between w-full">
            <span>By {schematic.author}</span>
            <span>{formatNumber(schematic.downloads)} downloads</span>
          </div>
        </CardFooter>
      </Card>
    </Link>
  );
};

Schematic Upload Form

Form for uploading new schematics:

// src/components/features/schematics/SchematicUploadForm.tsx
import { useState } from 'react';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { schematicSchema } from '@/schemas/schematic.schema';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { useUploadSchematic } from '@/api/endpoints/useSchematics';
import { FileUploader } from '@/components/ui/file-uploader';
import { useToast } from '@/hooks/useToast';

export const SchematicUploadForm = () => {
  const { toast } = useToast();
  const [schematicFile, setSchematicFile] = useState<File | null>(null);
  const [previewImage, setPreviewImage] = useState<File | null>(null);
  
  const { register, handleSubmit, formState: { errors } } = useForm({
    resolver: zodResolver(schematicSchema),
  });
  
  const uploadMutation = useUploadSchematic();
  
  const onSubmit = async (data) => {
    if (!schematicFile) {
      toast({
        title: "Error",
        description: "Please upload a schematic file",
        variant: "destructive",
      });
      return;
    }
    
    try {
      await uploadMutation.mutateAsync({
        ...data,
        schematicFile,
        previewImage,
      });
      
      toast({
        title: "Success",
        description: "Your schematic has been uploaded",
      });
      
      // Reset form or redirect
    } catch (error) {
      toast({
        title: "Error",
        description: `Failed to upload schematic: ${error.message}`,
        variant: "destructive",
      });
    }
  };
  
  // Form implementation
  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
      {/* Form fields */}
    </form>
  );
};

Schematic Detail View

Component for displaying detailed schematic information:

// src/components/features/schematics/SchematicDetail.tsx
import { useParams } from 'react-router-dom';
import { useFetchSchematic } from '@/api/endpoints/useSchematics';
import { Button } from '@/components/ui/button';
import { LoadingOverlay } from '@/components/loading-overlays/LoadingOverlay';
import { SchematicRating } from './SchematicRating';
import { SchematicComments } from './SchematicComments';

export const SchematicDetail = () => {
  const { id } = useParams<{ id: string }>();
  const { data: schematic, isLoading, error } = useFetchSchematic(id);
  
  if (isLoading) return <LoadingOverlay />;
  if (error) return <ErrorMessage error={error} />;
  if (!schematic) return <NotFound message="Schematic not found" />;
  
  return (
    <div className="container mx-auto py-6">
      {/* Schematic details implementation */}
    </div>
  );
};

State Management

Schematic state is managed through custom hooks:

  • useSchematics: Handles CRUD operations for schematics
  • useSearchSchematics: Manages search and filtering
  • useSchematicRating: Handles rating functionality

Data Model

Schematics follow the SchematicSchema defined in /src/schemas/schematic.schema.ts:

// src/schemas/schematic.schema.ts
import { z } from 'zod';

export const schematicSchema = z.object({
  $id: z.string().optional(),
  name: z.string().min(3, "Name must be at least 3 characters").max(100),
  description: z.string().min(10, "Description must be at least 10 characters").max(1000),
  author: z.string(),
  tags: z.array(z.string()).min(1, "Add at least one tag"),
  minecraft_version: z.string(),
  create_version: z.string(),
  file_url: z.string().optional(),
  preview_url: z.string().optional(),
  downloads: z.number().default(0),
  ratings: z.number().default(0),
  rating_count: z.number().default(0),
  created_at: z.string().optional(),
  updated_at: z.string().optional(),
});

export type Schematic = z.infer<typeof schematicSchema>;

API Integration

Appwrite Integration

Schematics are stored in the Appwrite database in the schematics collection, with schematic files and preview images stored in Appwrite Storage.

Meilisearch Integration

Schematics are indexed in Meilisearch for fast search and filtering by name, description, tags, and other attributes.

Implementation Details

File Handling

Schematic files (.nbt or .schem) are handled specially:

  1. Upload Process:
    • File is uploaded to Appwrite Storage
    • File URL is stored in the schematic document
    • Preview image is processed and optimized
// Example file upload implementation
const uploadFile = async (file: File, bucketId: string) => {
  try {
    const compressedFile = await compressImage(file);
    const fileId = ID.unique();
    const result = await storage.createFile(bucketId, fileId, compressedFile);
    return storage.getFileView(bucketId, fileId);
  } catch (error) {
    console.error('Error uploading file:', error);
    throw new Error('Failed to upload file. Please try again.');
  }
};
  1. Download Process:
    • User requests download
    • Download count is incremented
    • File is served from Appwrite Storage
// Example download implementation
const downloadSchematic = async (schematicId: string) => {
  try {
    // Get schematic data
    const schematic = await databases.getDocument(
      DATABASE_ID,
      SCHEMATICS_COLLECTION_ID,
      schematicId
    );
    
    // Increment download count
    await databases.updateDocument(
      DATABASE_ID,
      SCHEMATICS_COLLECTION_ID,
      schematicId,
      {
        downloads: schematic.downloads + 1
      }
    );
    
    // Return file URL
    return schematic.file_url;
  } catch (error) {
    console.error('Error downloading schematic:', error);
    throw new Error('Failed to download schematic. Please try again.');
  }
};

Rating System

The rating system allows users to rate schematics:

  1. Rating Process:
    • User submits a rating (1-5 stars)
    • System checks if user has already rated
    • Rating is stored and average is updated
// Example rating implementation
const rateSchematic = async (schematicId: string, rating: number, userId: string) => {
  try {
    // Check if user has already rated
    const existingRating = await databases.listDocuments(
      DATABASE_ID,
      RATINGS_COLLECTION_ID,
      [
        Query.equal('schematic_id', schematicId),
        Query.equal('user_id', userId)
      ]
    );
    
    // Handle existing rating (update) or create new rating
    if (existingRating.documents.length > 0) {
      // Update existing rating
    } else {
      // Create new rating
    }
    
    // Update schematic average rating
    // ...
    
    return { success: true };
  } catch (error) {
    console.error('Error rating schematic:', error);
    throw new Error('Failed to submit rating. Please try again.');
  }
};

Security Considerations

  1. File Validation: Schematic files are validated for format and size

    const validateSchematicFile = (file: File) => {
      // Check file size
      if (file.size > MAX_SCHEMATIC_SIZE) {
        throw new Error(`File too large. Maximum size is ${MAX_SCHEMATIC_SIZE / 1024 / 1024}MB`);
      }
      
      // Check file extension
      const extension = file.name.split('.').pop()?.toLowerCase();
      if (!['nbt', 'schem', 'schematic'].includes(extension || '')) {
        throw new Error('Invalid file format. Only .nbt, .schem, or .schematic files are allowed.');
      }
      
      return true;
    };
  2. Permission Management: Only owners can edit/delete schematics

    // Check if user is the owner of the schematic
    const isOwner = (schematic, userId) => {
      return schematic.user_id === userId;
    };
  3. Content Moderation: Reporting system for inappropriate content

    const reportSchematic = async (schematicId: string, reason: string, userId: string) => {
      // Create report in database
      // ...
    };

Performance Considerations

  1. Lazy Loading: Images are lazy-loaded for better performance

    <img 
      src={schematic.preview_url} 
      alt={schematic.name} 
      loading="lazy" 
      className="..."
    />
  2. Pagination: Results are paginated to improve load times

    <Pagination
      currentPage={page}
      totalPages={totalPages}
      onPageChange={setPage}
    />
  3. Image Optimization: Preview images are optimized before upload

    const optimizeImage = async (file: File): Promise<File> => {
      // Image compression logic
      // ...
      return compressedFile;
    };

Routing

Schematics feature uses the following routes:

// src/routes/schematicRoutes.tsx
import { lazy } from 'react';
import { RouteObject } from 'react-router';

const SchematicDetail = lazy(() => import('@/pages/schematics/SchematicDetail'));
const SchematicUpload = lazy(() => import('@/pages/schematics/SchematicUpload'));
const SchematicEdit = lazy(() => import('@/pages/schematics/SchematicEdit'));
const UserSchematics = lazy(() => import('@/pages/schematics/UserSchematics'));

export const schematicRoutes: RouteObject[] = [
  {
    path: 'schematics/:id',
    element: <SchematicDetail />,
  },
  {
    path: 'schematics/upload',
    element: <SchematicUpload />,
  },
  {
    path: 'schematics/edit/:id',
    element: <SchematicEdit />,
  },
  {
    path: 'user/schematics',
    element: <UserSchematics />,
  },
];

Best Practices

  1. Validate User Input: Always validate input on both client and server

    // Client-side validation with Zod
    const schema = z.object({
      name: z.string().min(3).max(100),
      // Other validations
    });
  2. Optimize Images: Compress and resize images before upload

    import imageCompression from 'browser-image-compression';
    
    const compressImage = async (file: File): Promise<File> => {
      const options = {
        maxSizeMB: 1,
        maxWidthOrHeight: 1200,
        useWebWorker: true,
      };
      
      try {
        return await imageCompression(file, options);
      } catch (error) {
        console.error('Error compressing image:', error);
        return file;
      }
    };
  3. Handle File Downloads Properly: Track downloads and provide feedback

    const handleDownload = async (schematicId: string) => {
      setDownloading(true);
      try {
        const url = await downloadSchematic(schematicId);
        
        // Create a hidden link element
        const a = document.createElement('a');
        a.href = url;
        a.download = 'schematic.nbt';
        document.body.appendChild(a);
        a.click();
        document.body.removeChild(a);
        
        toast({
          title: "Success",
          description: "Download started",
        });
      } catch (error) {
        toast({
          title: "Error",
          description: `Download failed: ${error.message}`,
          variant: "destructive",
        });
      } finally {
        setDownloading(false);
      }
    };

Related Documentation

⚠️ **GitHub.com Fallback** ⚠️