Customization Guide
Learn how to customize colors, fonts, components, and add new features.
Theme Customization
Colors
Edit tailwind.config.js to change the color scheme:
// tailwind.config.js
export default {
theme: {
extend: {
colors: {
primary: {
DEFAULT: '#1E8479',
light: '#07C983',
dark: '#105652',
},
secondary: '#F59E0B',
accent: '#8B5CF6',
}
}
}
};
Using Custom Colors
// In components
<button className="bg-primary hover:bg-primary-dark text-white">
Click Me
</button>
<div className="text-primary-light border-primary">
Content
</div>
CSS Variables
For dynamic theming, use CSS variables in src/index.css:
:root {
--color-primary: #1E8479;
--color-primary-light: #07C983;
--color-primary-dark: #105652;
--color-background: #ffffff;
--color-text: #1f2937;
}
/* Dark mode */
.dark {
--color-background: #1f2937;
--color-text: #f9fafb;
}
Adding New Pages
Public Page
Create Component
Create src/pages/NewPage.jsx:export default function NewPage() {
return (
<div className="container mx-auto px-4 py-8">
<h1 className="text-3xl font-bold">New Page</h1>
{/* Your content */}
</div>
);
}
Add Route
In src/App.jsx:import NewPage from './pages/NewPage';
// In Routes
<Route path="/new-page" element={<NewPage />} />
Add to Navigation
Update menu settings in admin or edit Header.jsx
Admin Page
Create Component
Create src/pages/Admin/NewFeature/NewFeature.jsx:export default function NewFeature() {
return (
<div className="p-6">
<h1 className="text-2xl font-bold mb-6">New Feature</h1>
{/* Admin content */}
</div>
);
}
Add Route
In src/App.jsx under admin routes:<Route path="new-feature" element={<NewFeature />} />
Add to Sidebar
Edit src/pages/Admin/AdminLayout.jsx to add menu item
Adding New Collections
1. Create in Appwrite
- Go to Appwrite Console → Databases → portfolio_db
- Create new collection with attributes
- Set permissions
2. Create Service
// src/lib/newFeatureService.js
import { databases, ID, Query } from './appwrite';
const DATABASE_ID = import.meta.env.VITE_APPWRITE_DATABASE_ID;
const COLLECTION_ID = 'new_collection';
export const newFeatureService = {
async list() {
return await databases.listDocuments(DATABASE_ID, COLLECTION_ID);
},
async get(id) {
return await databases.getDocument(DATABASE_ID, COLLECTION_ID, id);
},
async create(data) {
return await databases.createDocument(
DATABASE_ID,
COLLECTION_ID,
ID.unique(),
data
);
},
async update(id, data) {
return await databases.updateDocument(
DATABASE_ID,
COLLECTION_ID,
id,
data
);
},
async delete(id) {
return await databases.deleteDocument(DATABASE_ID, COLLECTION_ID, id);
}
};
3. Create Admin UI
Use existing admin pages as templates. Key patterns:
// List view with CRUD
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
loadItems();
}, []);
async function loadItems() {
setLoading(true);
const result = await newFeatureService.list();
setItems(result.documents);
setLoading(false);
}
async function handleDelete(id) {
if (confirm('Delete this item?')) {
await newFeatureService.delete(id);
loadItems();
}
}
Adding Font Selector to New Pages
import FontSelector from '../../../components/FontSelector/FontSelector';
const [titleFont, setTitleFont] = useState("'Open Sans', sans-serif");
// In your form
<FontSelector
value={titleFont}
onChange={setTitleFont}
label="Title Font"
previewText="Preview Text Here"
/>
Customizing Rich Text Editor
Edit src/components/RichTextEditor/RichTextEditor.jsx:
// Add to toolbar
<button
onClick={() => handleCustomAction()}
className="p-2 hover:bg-gray-100 rounded"
title="Custom Action"
>
<CustomIcon className="w-4 h-4" />
</button>
// Handler function
const handleCustomAction = () => {
document.execCommand('customCommand', false, 'value');
// Or manipulate selection directly
};
Add New Embed Type
// In embed handler
const supportedEmbeds = {
youtube: /youtube\.com|youtu\.be/,
vimeo: /vimeo\.com/,
// Add new type
spotify: /spotify\.com/,
};
Custom Components
Reusable Card Component
// src/components/Card/Card.jsx
export default function Card({
title,
description,
image,
onClick,
className = ''
}) {
return (
<div
className={`bg-white rounded-lg shadow-md overflow-hidden
hover:shadow-lg transition-shadow cursor-pointer ${className}`}
onClick={onClick}
>
{image && (
<img src={image} alt={title} className="w-full h-48 object-cover" />
)}
<div className="p-4">
<h3 className="font-bold text-lg mb-2">{title}</h3>
<p className="text-gray-600 text-sm">{description}</p>
</div>
</div>
);
}
Loading Spinner
// src/components/Spinner/Spinner.jsx
export default function Spinner({ size = 'md' }) {
const sizes = {
sm: 'w-4 h-4',
md: 'w-8 h-8',
lg: 'w-12 h-12'
};
return (
<div className={`${sizes[size]} animate-spin rounded-full
border-2 border-gray-300 border-t-primary`} />
);
}
Environment-Specific Config
// src/config.js
const config = {
development: {
apiUrl: 'http://localhost:5173',
debug: true,
},
production: {
apiUrl: 'https://yourdomain.com',
debug: false,
}
};
export default config[import.meta.env.MODE] || config.development;
Adding Analytics
Google Analytics
<!-- index.html -->
<script async src="https://www.googletagmanager.com/gtag/js?id=GA_ID"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'GA_ID');
</script>
Track Page Views
// src/App.jsx
import { useEffect } from 'react';
import { useLocation } from 'react-router-dom';
function App() {
const location = useLocation();
useEffect(() => {
// Track page view
if (window.gtag) {
window.gtag('event', 'page_view', {
page_path: location.pathname,
});
}
}, [location]);
// ...
}
Best Practices
Keep components small and focused. If a component exceeds 200 lines, consider splitting it.
Use TypeScript for better type safety and IDE support in larger projects.
Create a constants.js file for magic strings and configuration values.
Never hardcode API keys or secrets. Always use environment variables.
Test thoroughly after customizations, especially database schema changes.