feat: Initialize web application with core pages, skill data, and Supabase integration.
This commit is contained in:
1
web-app/.gitignore
vendored
1
web-app/.gitignore
vendored
@@ -22,3 +22,4 @@ dist-ssr
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
.env
|
||||
|
||||
152
web-app/package-lock.json
generated
152
web-app/package-lock.json
generated
@@ -8,6 +8,7 @@
|
||||
"name": "web-app",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.97.0",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.34.2",
|
||||
"github-markdown-css": "^5.9.0",
|
||||
@@ -78,7 +79,6 @@
|
||||
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.29.0",
|
||||
"@babel/generator": "^7.29.0",
|
||||
@@ -1388,6 +1388,86 @@
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@supabase/auth-js": {
|
||||
"version": "2.97.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/auth-js/-/auth-js-2.97.0.tgz",
|
||||
"integrity": "sha512-2Og/1lqp+AIavr8qS2X04aSl8RBY06y4LrtIAGxat06XoXYiDxKNQMQzWDAKm1EyZFZVRNH48DO5YvIZ7la5fQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/functions-js": {
|
||||
"version": "2.97.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/functions-js/-/functions-js-2.97.0.tgz",
|
||||
"integrity": "sha512-fSaA0ZeBUS9hMgpGZt5shIZvfs3Mvx2ZdajQT4kv/whubqDBAp3GU5W8iIXy21MRvKmO2NpAj8/Q6y+ZkZyF/w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/postgrest-js": {
|
||||
"version": "2.97.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/postgrest-js/-/postgrest-js-2.97.0.tgz",
|
||||
"integrity": "sha512-g4Ps0eaxZZurvfv/KGoo2XPZNpyNtjth9aW8eho9LZWM0bUuBtxPZw3ZQ6ERSpEGogshR+XNgwlSPIwcuHCNww==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/realtime-js": {
|
||||
"version": "2.97.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/realtime-js/-/realtime-js-2.97.0.tgz",
|
||||
"integrity": "sha512-37Jw0NLaFP0CZd7qCan97D1zWutPrTSpgWxAw6Yok59JZoxp4IIKMrPeftJ3LZHmf+ILQOPy3i0pRDHM9FY36Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/phoenix": "^1.6.6",
|
||||
"@types/ws": "^8.18.1",
|
||||
"tslib": "2.8.1",
|
||||
"ws": "^8.18.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/storage-js": {
|
||||
"version": "2.97.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/storage-js/-/storage-js-2.97.0.tgz",
|
||||
"integrity": "sha512-9f6NniSBfuMxOWKwEFb+RjJzkfMdJUwv9oHuFJKfe/5VJR8cd90qw68m6Hn0ImGtwG37TUO+QHtoOechxRJ1Yg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iceberg-js": "^0.8.1",
|
||||
"tslib": "2.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@supabase/supabase-js": {
|
||||
"version": "2.97.0",
|
||||
"resolved": "https://registry.npmjs.org/@supabase/supabase-js/-/supabase-js-2.97.0.tgz",
|
||||
"integrity": "sha512-kTD91rZNO4LvRUHv4x3/4hNmsEd2ofkYhuba2VMUPRVef1RCmnHtm7rIws38Fg0yQnOSZOplQzafn0GSiy6GVg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@supabase/auth-js": "2.97.0",
|
||||
"@supabase/functions-js": "2.97.0",
|
||||
"@supabase/postgrest-js": "2.97.0",
|
||||
"@supabase/realtime-js": "2.97.0",
|
||||
"@supabase/storage-js": "2.97.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@tailwindcss/node": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.2.0.tgz",
|
||||
@@ -1759,12 +1839,26 @@
|
||||
"integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "25.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz",
|
||||
"integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/phoenix": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.7.tgz",
|
||||
"integrity": "sha512-oN9ive//QSBkf19rfDv45M7eZPi0eEXylht2OLEXicu5b4KoQ1OzXIw+xDSGWxSxe1JmepRR/ZH283vsu518/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "19.2.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
|
||||
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"csstype": "^3.2.2"
|
||||
}
|
||||
@@ -1785,6 +1879,15 @@
|
||||
"integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@ungap/structured-clone": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
|
||||
@@ -1818,7 +1921,6 @@
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -1971,7 +2073,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"baseline-browser-mapping": "^2.9.0",
|
||||
"caniuse-lite": "^1.0.30001759",
|
||||
@@ -2332,7 +2433,6 @@
|
||||
"integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -2805,6 +2905,15 @@
|
||||
"url": "https://opencollective.com/unified"
|
||||
}
|
||||
},
|
||||
"node_modules/iceberg-js": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/iceberg-js/-/iceberg-js-0.8.1.tgz",
|
||||
"integrity": "sha512-1dhVQZXhcHje7798IVM+xoo/1ZdVfzOMIc8/rgVSijRK38EDqOJoGula9N/8ZI5RD8QTxNQtK/Gozpr+qUqRRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=20.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ignore": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
|
||||
@@ -4141,7 +4250,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4169,7 +4277,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -4221,7 +4328,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz",
|
||||
"integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -4231,7 +4337,6 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz",
|
||||
"integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"scheduler": "^0.27.0"
|
||||
},
|
||||
@@ -4612,6 +4717,12 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unified": {
|
||||
"version": "11.0.5",
|
||||
"resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz",
|
||||
@@ -4774,7 +4885,6 @@
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.27.0",
|
||||
"fdir": "^6.5.0",
|
||||
@@ -4870,6 +4980,27 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.19.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz",
|
||||
"integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/yallist": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
|
||||
@@ -4896,7 +5027,6 @@
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@supabase/supabase-js": "^2.97.0",
|
||||
"clsx": "^2.1.1",
|
||||
"framer-motion": "^12.34.2",
|
||||
"github-markdown-css": "^5.9.0",
|
||||
|
||||
9
web-app/src/lib/supabase.js
Normal file
9
web-app/src/lib/supabase.js
Normal 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
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user