features - blueprint-site/blueprint-create GitHub Wiki
The Addons feature is a core part of Blueprint, allowing users to discover, browse, and filter Create Mod addons.
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 navigates to the Addons page (
/addons
) - 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
- Results update in real-time as filters are applied
- Clicking on an addon navigates to its details page
- User navigates to an addon details page (
/addons/:slug
) - 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)
- User can download the addon from the original source
- User can add the addon to a personal collection
- User clicks "Add to Collection" button on an addon card or details page
- If not logged in, user is prompted to log in
- User selects an existing collection or creates a new one
- Addon is added to the collection
- Confirmation is shown to the user
- 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
- 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
-
- 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
-
- 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
-
Addon state is managed through custom hooks:
// Example usage
const AddonListPage = () => {
const { filters, setFilter, resetFilters } = useFilters();
const { data, isLoading, error } = useSearchAddons(filters);
// Component implementation
}
- 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
-
- 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
-
- 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
-
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
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;
}
};
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;
}
};
Addon data is sourced from:
-
CurseForge API
- Provides comprehensive addon metadata
- Includes download statistics
- Offers detailed version compatibility
-
Modrinth API
- Alternative source for modding content
- Different community focus
- Additional compatibility information
-
Manual Submissions
- For addons not available on major platforms
- Community-contributed information
- Admin-verified data
Addon data is synchronized between Appwrite and Meilisearch:
- Data is written to or updated in Appwrite
- A background process detects changes
- Updated data is sent to Meilisearch
- 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.
- 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
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>
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
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]);
- Check if Meilisearch is properly configured
- Verify the index exists and contains data
- Ensure filters aren't too restrictive
- Check console for API errors
- Verify the slug is correct
- Check if the addon exists in Appwrite
- Ensure all required fields are present
- Check for permissions issues
- Verify filterable attributes in Meilisearch
- Check the filter syntax
- Ensure the filter values exist in the data
- Look for JavaScript errors in the console
- 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
Planned improvements for the Addons feature:
- Version Compatibility Matrix: Visual display of compatibility across versions
- Change Tracking: History of updates and changes to addons
- Dependency Visualization: Graph view of addon dependencies
- Community Ratings: User-provided ratings and reviews
- Collection Sharing: Public sharing of addon collections
- Mod Pack Integration: Creating mod packs from addon collections
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.
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 navigates to the Schematics page
- 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
- User clicks "Upload Schematic" button
- User is prompted to log in if not already authenticated
- User fills out the upload form:
- Schematic name
- Description
- Tags/categories
- Minecraft version
- Create Mod version
- Schematic file (.nbt or .schem)
- Preview image(s)
- User submits the form
- System processes and validates the schematic
- Confirmation is shown to the user
- User clicks on a schematic card
- 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
- User can download the schematic file
- User can rate the schematic
- User can leave comments if authenticated
- User navigates to their profile
- User selects "My Schematics" tab
- User can:
- View all their uploaded schematics
- Edit existing schematics
- Delete their schematics
- See download and rating statistics
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>
);
};
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>
);
};
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>
);
};
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>
);
};
Schematic state is managed through custom hooks:
-
useSchematics
: Handles CRUD operations for schematics -
useSearchSchematics
: Manages search and filtering -
useSchematicRating
: Handles rating functionality
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>;
Schematics are stored in the Appwrite database in the schematics
collection, with schematic files and preview images stored in Appwrite Storage.
Schematics are indexed in Meilisearch for fast search and filtering by name, description, tags, and other attributes.
Schematic files (.nbt or .schem) are handled specially:
-
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.');
}
};
-
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.');
}
};
The rating system allows users to rate schematics:
-
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.');
}
};
-
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; };
-
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; };
-
Content Moderation: Reporting system for inappropriate content
const reportSchematic = async (schematicId: string, reason: string, userId: string) => { // Create report in database // ... };
-
Lazy Loading: Images are lazy-loaded for better performance
<img src={schematic.preview_url} alt={schematic.name} loading="lazy" className="..." />
-
Pagination: Results are paginated to improve load times
<Pagination currentPage={page} totalPages={totalPages} onPageChange={setPage} />
-
Image Optimization: Preview images are optimized before upload
const optimizeImage = async (file: File): Promise<File> => { // Image compression logic // ... return compressedFile; };
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 />,
},
];
-
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 });
-
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; } };
-
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); } };