feat: Initialize web application with core pages, skill data, and Supabase integration.

This commit is contained in:
Zied
2026-02-25 17:56:20 +01:00
parent 7c4aa1c88b
commit 68266007b9
6 changed files with 308 additions and 45 deletions

View File

@@ -0,0 +1,9 @@
import { createClient } from '@supabase/supabase-js'
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL || 'https://gczhgcbtjbvfrgfmpbmv.supabase.co';
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY || 'sb_publishable_CyVwHGbtT80AuDFmXNkc9Q_YNcamTGg';
// Create a single supabase client for interacting with your database
export const supabase = supabaseUrl && supabaseAnonKey
? createClient(supabaseUrl, supabaseAnonKey)
: null

View File

@@ -1,8 +1,8 @@
import { useState, useEffect } from 'react';
import { Link } from 'react-router-dom';
import { Search, Filter, Book, AlertCircle, ArrowRight } from 'lucide-react';
import { Search, Filter, Book, AlertCircle, ArrowRight, Star } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import { supabase } from '../lib/supabase';
export function Home() {
const [skills, setSkills] = useState([]);
@@ -10,21 +10,84 @@ export function Home() {
const [search, setSearch] = useState('');
const [categoryFilter, setCategoryFilter] = useState('all');
const [loading, setLoading] = useState(true);
const [stars, setStars] = useState({});
useEffect(() => {
fetch('/skills.json')
.then(res => res.json())
.then(data => {
const fetchSkillsAndStars = async () => {
try {
// Fetch basic skill data
const res = await fetch('/skills.json');
const data = await res.json();
setSkills(data);
setFilteredSkills(data);
setLoading(false);
})
.catch(err => {
// Fetch star counts if supabase is configured
if (supabase) {
const { data: starData, error } = await supabase
.from('skill_stars')
.select('skill_id, star_count');
if (!error && starData) {
const starMap = {};
starData.forEach(item => {
starMap[item.skill_id] = item.star_count;
});
setStars(starMap);
}
}
} catch (err) {
console.error("Failed to load skills", err);
} finally {
setLoading(false);
});
}
};
fetchSkillsAndStars();
}, []);
const handleStarClick = async (e, skillId) => {
e.preventDefault(); // Prevent link navigation
// Basic check to prevent spamming from same browser
const storedStars = JSON.parse(localStorage.getItem('user_stars') || '{}');
if (storedStars[skillId]) return;
// Optimistically update UI
setStars(prev => ({
...prev,
[skillId]: (prev[skillId] || 0) + 1
}));
// Remember locally
localStorage.setItem('user_stars', JSON.stringify({
...storedStars,
[skillId]: true
}));
if (supabase) {
// First try to select existing
const { data } = await supabase
.from('skill_stars')
.select('star_count')
.eq('skill_id', skillId)
.single();
if (data) {
// Update existing
await supabase
.from('skill_stars')
.update({ star_count: data.star_count + 1 })
.eq('skill_id', skillId);
} else {
// Insert new
await supabase
.from('skill_stars')
.insert({ skill_id: skillId, star_count: 1 });
}
}
};
useEffect(() => {
let result = skills;
@@ -115,6 +178,14 @@ export function Home() {
{skill.category || 'Uncategorized'}
</span>
</div>
<button
onClick={(e) => handleStarClick(e, skill.id)}
className="flex items-center space-x-1 px-2 py-1 rounded-md bg-slate-50 dark:bg-slate-800/50 hover:bg-yellow-50 dark:hover:bg-yellow-900/20 text-slate-500 hover:text-yellow-600 dark:hover:text-yellow-500 transition-colors border border-slate-200 dark:border-slate-800 z-10"
title="Upvote skill"
>
<Star className={`h-4 w-4 ${JSON.parse(localStorage.getItem('user_stars') || '{}')[skill.id] ? 'fill-yellow-400 stroke-yellow-400' : ''}`} />
<span className="text-xs font-semibold">{stars[skill.id] || 0}</span>
</button>
</div>
<h3 className="text-lg font-bold text-slate-900 dark:text-slate-50 group-hover:text-indigo-600 dark:group-hover:text-indigo-400 transition-colors mb-2 line-clamp-1">

View File

@@ -1,8 +1,8 @@
import { useState, useEffect } from 'react';
import { useParams, Link } from 'react-router-dom';
import Markdown from 'react-markdown';
import { ArrowLeft, Copy, Check, FileCode, AlertTriangle } from 'lucide-react';
import { ArrowLeft, Copy, Check, FileCode, AlertTriangle, Star } from 'lucide-react';
import { supabase } from '../lib/supabase';
export function SkillDetail() {
const { id } = useParams();
@@ -13,44 +13,88 @@ export function SkillDetail() {
const [copiedFull, setCopiedFull] = useState(false);
const [error, setError] = useState(null);
const [customContext, setCustomContext] = useState('');
const [starCount, setStarCount] = useState(0);
useEffect(() => {
// 1. Fetch index to get skill metadata and path
fetch('/skills.json')
.then(res => res.json())
.then(skills => {
// Fetch index and stars in parallel when possible
const loadData = async () => {
try {
// 1. Fetch index to get skill metadata and path
const res = await fetch('/skills.json');
const skills = await res.json();
const foundSkill = skills.find(s => s.id === id);
if (foundSkill) {
setSkill(foundSkill);
// Fetch star count
if (supabase) {
const { data } = await supabase
.from('skill_stars')
.select('star_count')
.eq('skill_id', id)
.single();
if (data) {
setStarCount(data.star_count);
}
}
// 2. Fetch the actual markdown content
// The path in JSON is like "skills/category/name"
// We mapped it to "/skills/..." in public folder
// Remove "skills/" prefix if it exists in path to avoid double
const cleanPath = foundSkill.path.startsWith('skills/')
? foundSkill.path.replace('skills/', '')
: foundSkill.path;
fetch(`/skills/${cleanPath}/SKILL.md`)
.then(res => {
if (!res.ok) throw new Error('Skill file not found');
return res.text();
})
.then(text => {
setContent(text);
setLoading(false);
})
.catch(err => {
console.error("Failed to load skill content", err);
setError("Could not load skill content. File might be missing.");
setLoading(false);
});
const mdRes = await fetch(`/skills/${cleanPath}/SKILL.md`);
if (!mdRes.ok) throw new Error('Skill file not found');
const text = await mdRes.text();
setContent(text);
} else {
setError("Skill not found in registry.");
setLoading(false);
}
});
} catch (err) {
console.error("Failed to load skill data", err);
setError(err.message || "Could not load skill content.");
} finally {
setLoading(false);
}
};
loadData();
}, [id]);
const handleStarClick = async () => {
const storedStars = JSON.parse(localStorage.getItem('user_stars') || '{}');
if (storedStars[id]) return;
// Optimistic UI updates
setStarCount(prev => prev + 1);
localStorage.setItem('user_stars', JSON.stringify({
...storedStars,
[id]: true
}));
if (supabase) {
const { data } = await supabase
.from('skill_stars')
.select('star_count')
.eq('skill_id', id)
.single();
if (data) {
await supabase
.from('skill_stars')
.update({ star_count: data.star_count + 1 })
.eq('skill_id', id);
} else {
await supabase
.from('skill_stars')
.insert({ skill_id: id, star_count: 1 });
}
}
};
const copyToClipboard = () => {
const basePrompt = `Use @${skill.name}`;
const finalPrompt = customContext.trim()
@@ -112,6 +156,13 @@ export function SkillDetail() {
{skill.source}
</span>
)}
<button
onClick={handleStarClick}
className="flex items-center space-x-1.5 px-3 py-1 bg-yellow-50 dark:bg-yellow-900/10 hover:bg-yellow-100 dark:hover:bg-yellow-900/30 text-yellow-700 dark:text-yellow-500 rounded-full text-xs font-bold border border-yellow-200 dark:border-yellow-700/50 transition-colors"
>
<Star className={`h-3.5 w-3.5 ${JSON.parse(localStorage.getItem('user_stars') || '{}')[id] ? 'fill-yellow-500 stroke-yellow-500' : ''}`} />
<span>{starCount} Upvotes</span>
</button>
</div>
<h1 className="text-3xl sm:text-4xl font-extrabold text-slate-900 dark:text-slate-50 tracking-tight">
@{skill.name}