mirror of
https://github.com/System-End/scraps.git
synced 2026-04-19 19:45:14 +00:00
add in backend
This commit is contained in:
parent
a4d9325ea6
commit
aa957dc93f
10 changed files with 545 additions and 9 deletions
|
|
@ -8,6 +8,7 @@
|
|||
"drizzle-orm": "^0.45.1",
|
||||
"elysia": "^1.4.22",
|
||||
"pg": "^8.17.2",
|
||||
"redis": "^5.10.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.8",
|
||||
|
|
@ -81,6 +82,16 @@
|
|||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.12", "", { "os": "win32", "cpu": "x64" }, "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA=="],
|
||||
|
||||
"@redis/bloom": ["@redis/bloom@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-doIF37ob+l47n0rkpRNgU8n4iacBlKM9xLiP1LtTZTvz8TloJB8qx/MgvhMhKdYG+CvCY2aPBnN2706izFn/4A=="],
|
||||
|
||||
"@redis/client": ["@redis/client@5.10.0", "", { "dependencies": { "cluster-key-slot": "1.1.2" } }, "sha512-JXmM4XCoso6C75Mr3lhKA3eNxSzkYi3nCzxDIKY+YOszYsJjuKbFgVtguVPbLMOttN4iu2fXoc2BGhdnYhIOxA=="],
|
||||
|
||||
"@redis/json": ["@redis/json@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-B2G8XlOmTPUuZtD44EMGbtoepQG34RCDXLZbjrtON1Djet0t5Ri7/YPXvL9aomXqP8lLTreaprtyLKF4tmXEEA=="],
|
||||
|
||||
"@redis/search": ["@redis/search@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-3SVcPswoSfp2HnmWbAGUzlbUPn7fOohVu2weUQ0S+EMiQi8jwjL+aN2p6V3TI65eNfVsJ8vyPvqWklm6H6esmg=="],
|
||||
|
||||
"@redis/time-series": ["@redis/time-series@5.10.0", "", { "peerDependencies": { "@redis/client": "^5.10.0" } }, "sha512-cPkpddXH5kc/SdRhF0YG0qtjL+noqFT0AcHbQ6axhsPsO7iqPi1cjxgdkE9TNeKiBUUdCaU1DbqkR/LzbzPBhg=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="],
|
||||
|
||||
"@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
|
||||
|
|
@ -97,6 +108,8 @@
|
|||
|
||||
"bun-types": ["bun-types@1.3.8", "", { "dependencies": { "@types/node": "*" } }, "sha512-fL99nxdOWvV4LqjmC+8Q9kW3M4QTtTR1eePs94v5ctGqU8OeceWrSUaRw3JYb7tU3FkMIAjkueehrHPPPGKi5Q=="],
|
||||
|
||||
"cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
|
@ -155,6 +168,8 @@
|
|||
|
||||
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||
|
||||
"redis": ["redis@5.10.0", "", { "dependencies": { "@redis/bloom": "5.10.0", "@redis/client": "5.10.0", "@redis/json": "5.10.0", "@redis/search": "5.10.0", "@redis/time-series": "5.10.0" } }, "sha512-0/Y+7IEiTgVGPrLFKy8oAEArSyEJkU0zvgV5xyi9NzNQ+SLZmyFbUsWIbgPcd4UdUh00opXGKlXJwMmsis5Byw=="],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
|
|
|||
|
|
@ -1,5 +1,12 @@
|
|||
import { Elysia } from 'elysia'
|
||||
import projects from './routes/projects'
|
||||
import news from './routes/news'
|
||||
|
||||
const app = new Elysia();
|
||||
const app = new Elysia()
|
||||
.use(projects)
|
||||
.use(news)
|
||||
.listen(3000)
|
||||
|
||||
export default app;
|
||||
console.log(`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`)
|
||||
|
||||
export default app
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
export const RequestAccessToken = async function(code: string): Promise<{
|
||||
access_token?: string,
|
||||
failure_reason?: string
|
||||
}> {
|
||||
try {
|
||||
const response = await fetch("https://auth.hackclub.com/oauth/token", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
code,
|
||||
|
||||
client_id: process.env.CLIENT_ID,
|
||||
client_secret: process.env.CLIENT_SECRET,
|
||||
|
||||
grant_type: "authorization_code",
|
||||
redirect_uri: process.env.REDIRECT_URI,
|
||||
})
|
||||
});
|
||||
|
||||
if (response.status !== 200)
|
||||
return { failure_reason: "" };
|
||||
|
||||
|
||||
} catch (err) {
|
||||
return { failure_reason: "Internal Error" };
|
||||
}
|
||||
}
|
||||
|
|
@ -4,11 +4,11 @@
|
|||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "bun --watch src/index.ts",
|
||||
"build": "bun build src/index.ts --target bun --outdir ./dist",
|
||||
"start": "NODE_ENV=production bun dist/index.js",
|
||||
"test": "bun test"
|
||||
},
|
||||
"dev": "bun --watch src/index.ts",
|
||||
"build": "bun build src/index.ts --target bun --outdir ./dist",
|
||||
"start": "NODE_ENV=production bun dist/index.js",
|
||||
"test": "bun test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bun": "^1.3.8",
|
||||
"@types/pg": "^8.16.0",
|
||||
|
|
@ -22,6 +22,7 @@
|
|||
"dotenv": "^17.2.3",
|
||||
"drizzle-orm": "^0.45.1",
|
||||
"elysia": "^1.4.22",
|
||||
"pg": "^8.17.2"
|
||||
"pg": "^8.17.2",
|
||||
"redis": "^5.10.0"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
33
backend/routes/news.ts
Normal file
33
backend/routes/news.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { Elysia } from "elysia";
|
||||
|
||||
const news = new Elysia({
|
||||
prefix: "/news"
|
||||
});
|
||||
|
||||
// GET /news - Get all news items
|
||||
news.get("/", async () => {
|
||||
// TODO: Fetch news from database
|
||||
// const newsItems = await db.select().from(newsTable).orderBy(desc(newsTable.date))
|
||||
// return newsItems
|
||||
|
||||
// Dummy data for now
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
date: "jan 21, 2026",
|
||||
content: "remember to stay drafty!"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: "jan 15, 2026",
|
||||
content: "new items added to the shop!"
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
date: "jan 10, 2026",
|
||||
content: "scraps is now live!"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
export default news;
|
||||
|
|
@ -4,8 +4,54 @@ const projects = new Elysia({
|
|||
prefix: "/projects"
|
||||
});
|
||||
|
||||
// project routes n stuff
|
||||
// GET /projects - Get all projects for the authenticated user
|
||||
projects.get("/", async ({ headers }) => {
|
||||
// TODO: Get user from auth header and fetch their projects from database
|
||||
// const userId = await getUserFromToken(headers.authorization)
|
||||
// const userProjects = await db.select().from(projectsTable).where(eq(projectsTable.userId, userId))
|
||||
// return userProjects
|
||||
|
||||
// Dummy data for now
|
||||
return [
|
||||
{
|
||||
id: 1,
|
||||
userId: 1,
|
||||
name: "Blueprint",
|
||||
description: "A hackathon project for AMD",
|
||||
imageUrl: "/hero.png",
|
||||
githubUrl: "https://github.com/hackclub/blueprint",
|
||||
hours: 24,
|
||||
hackatimeUrl: "https://hackatime.hackclub.com/projects/blueprint"
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userId: 1,
|
||||
name: "Flavortown",
|
||||
description: "A food discovery app",
|
||||
imageUrl: "/hero.png",
|
||||
githubUrl: "https://github.com/hackclub/flavortown",
|
||||
hours: 18,
|
||||
hackatimeUrl: "https://hackatime.hackclub.com/projects/flavortown"
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// PUT /projects/:id - Update a project
|
||||
projects.put("/:id", async ({ params, body }) => {
|
||||
// TODO: Validate user owns this project and update in database
|
||||
// const project = await db.update(projectsTable).set(body).where(eq(projectsTable.id, params.id)).returning()
|
||||
// return project
|
||||
|
||||
return { success: true, ...body }
|
||||
});
|
||||
|
||||
// POST /projects - Create a new project
|
||||
projects.post("/", async ({ body }) => {
|
||||
// TODO: Get user from auth and create project in database
|
||||
// const project = await db.insert(projectsTable).values({ ...body, userId }).returning()
|
||||
// return project
|
||||
|
||||
return { success: true, id: Date.now(), ...body }
|
||||
});
|
||||
|
||||
export default projects;
|
||||
55
frontend/src/lib/components/DashboardNavbar.svelte
Normal file
55
frontend/src/lib/components/DashboardNavbar.svelte
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<script lang="ts">
|
||||
import { LayoutDashboard, Trophy, Store, Nut } from '@lucide/svelte'
|
||||
|
||||
let { screws = 0 }: { screws?: number } = $props()
|
||||
|
||||
let activeTab = $state('dashboard')
|
||||
</script>
|
||||
|
||||
<nav
|
||||
class="fixed top-0 left-0 right-0 z-50 flex items-center justify-between px-6 py-4 md:w-3/4 md:mx-auto md:left-1/2 md:-translate-x-1/2"
|
||||
>
|
||||
<a href="/">
|
||||
<img src="/flag-standalone-bw.png" alt="Hack Club" class="h-8 md:h-10" />
|
||||
</a>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<button
|
||||
onclick={() => (activeTab = 'dashboard')}
|
||||
class="flex items-center gap-2 px-6 py-2 border-4 rounded-full transition-all duration-300 {activeTab ===
|
||||
'dashboard'
|
||||
? 'bg-black text-white border-black'
|
||||
: 'border-black hover:border-dashed'}"
|
||||
>
|
||||
<LayoutDashboard size={18} />
|
||||
<span class="text-lg font-bold">dashboard</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={() => (activeTab = 'leaderboard')}
|
||||
class="flex items-center gap-2 px-6 py-2 border-4 rounded-full transition-all duration-300 {activeTab ===
|
||||
'leaderboard'
|
||||
? 'bg-black text-white border-black'
|
||||
: 'border-black hover:border-dashed'}"
|
||||
>
|
||||
<Trophy size={18} />
|
||||
<span class="text-lg font-bold">leaderboard</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onclick={() => (activeTab = 'shop')}
|
||||
class="flex items-center gap-2 px-6 py-2 border-4 rounded-full transition-all duration-300 {activeTab ===
|
||||
'shop'
|
||||
? 'bg-black text-white border-black'
|
||||
: 'border-black hover:border-dashed'}"
|
||||
>
|
||||
<Store size={18} />
|
||||
<span class="text-lg font-bold">shop</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-2">
|
||||
<Nut size={24} />
|
||||
<span class="text-xl font-bold">{screws}</span>
|
||||
</div>
|
||||
</nav>
|
||||
141
frontend/src/lib/components/ProjectModal.svelte
Normal file
141
frontend/src/lib/components/ProjectModal.svelte
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
<script lang="ts">
|
||||
import { X } from '@lucide/svelte'
|
||||
|
||||
interface Project {
|
||||
id: number
|
||||
userId: number
|
||||
name: string
|
||||
description: string
|
||||
imageUrl: string
|
||||
githubUrl: string
|
||||
hours: number
|
||||
hackatimeUrl: string
|
||||
}
|
||||
|
||||
let {
|
||||
project = $bindable<Project | null>(null),
|
||||
onClose,
|
||||
onSave
|
||||
}: {
|
||||
project: Project | null
|
||||
onClose: () => void
|
||||
onSave: (project: Project) => void
|
||||
} = $props()
|
||||
|
||||
let editedProject = $state<Project | null>(null)
|
||||
|
||||
$effect(() => {
|
||||
if (project) {
|
||||
editedProject = { ...project }
|
||||
}
|
||||
})
|
||||
|
||||
function handleSave() {
|
||||
if (editedProject) {
|
||||
// TODO: Call API to update project
|
||||
onSave(editedProject)
|
||||
}
|
||||
}
|
||||
|
||||
function handleBackdropClick(e: MouseEvent) {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if project && editedProject}
|
||||
<div
|
||||
class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4"
|
||||
onclick={handleBackdropClick}
|
||||
onkeydown={(e) => e.key === 'Escape' && onClose()}
|
||||
role="dialog"
|
||||
tabindex="-1"
|
||||
>
|
||||
<div class="bg-white rounded-2xl w-full max-w-lg p-6 border-4 border-black">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-2xl font-bold">edit draft</h2>
|
||||
<button onclick={onClose} class="p-1 hover:bg-gray-100 rounded-lg transition-colors">
|
||||
<X size={24} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-bold mb-1">name</label>
|
||||
<input
|
||||
id="name"
|
||||
type="text"
|
||||
bind:value={editedProject.name}
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="description" class="block text-sm font-bold mb-1">description</label>
|
||||
<textarea
|
||||
id="description"
|
||||
bind:value={editedProject.description}
|
||||
rows="3"
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed resize-none"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="imageUrl" class="block text-sm font-bold mb-1">image url</label>
|
||||
<input
|
||||
id="imageUrl"
|
||||
type="url"
|
||||
bind:value={editedProject.imageUrl}
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="githubUrl" class="block text-sm font-bold mb-1">github url</label>
|
||||
<input
|
||||
id="githubUrl"
|
||||
type="url"
|
||||
bind:value={editedProject.githubUrl}
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="hackatimeUrl" class="block text-sm font-bold mb-1">hackatime url</label>
|
||||
<input
|
||||
id="hackatimeUrl"
|
||||
type="url"
|
||||
bind:value={editedProject.hackatimeUrl}
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="hours" class="block text-sm font-bold mb-1">hours</label>
|
||||
<input
|
||||
id="hours"
|
||||
type="number"
|
||||
bind:value={editedProject.hours}
|
||||
class="w-full px-4 py-2 border-2 border-black rounded-lg focus:outline-none focus:border-dashed"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3 mt-6">
|
||||
<button
|
||||
onclick={onClose}
|
||||
class="flex-1 px-4 py-2 border-2 border-black rounded-full font-bold hover:border-dashed transition-all"
|
||||
>
|
||||
cancel
|
||||
</button>
|
||||
<button
|
||||
onclick={handleSave}
|
||||
class="flex-1 px-4 py-2 bg-black text-white rounded-full font-bold hover:bg-gray-800 transition-all"
|
||||
>
|
||||
save
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
17
frontend/src/lib/components/RandomPhrase.svelte
Normal file
17
frontend/src/lib/components/RandomPhrase.svelte
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<script lang="ts">
|
||||
const phrases = [
|
||||
'fantasy football',
|
||||
'scrap that, ship this',
|
||||
'turning leftovers into legendary',
|
||||
'one persons scraps, anothers treasure',
|
||||
'built different, shipped anyway',
|
||||
'ctrl+z your regrets',
|
||||
'powered by fudge and caffeine',
|
||||
'404 boredom not found',
|
||||
'shipping since 2025'
|
||||
]
|
||||
|
||||
const randomPhrase = phrases[Math.floor(Math.random() * phrases.length)]
|
||||
</script>
|
||||
|
||||
<span>{randomPhrase}</span>
|
||||
195
frontend/src/routes/dashboard/+page.svelte
Normal file
195
frontend/src/routes/dashboard/+page.svelte
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte'
|
||||
import { FilePlus2, Pencil } from '@lucide/svelte'
|
||||
import DashboardNavbar from '$lib/components/DashboardNavbar.svelte'
|
||||
import ProjectModal from '$lib/components/ProjectModal.svelte'
|
||||
import RandomPhrase from '$lib/components/RandomPhrase.svelte'
|
||||
|
||||
interface Project {
|
||||
id: number
|
||||
userId: number
|
||||
name: string
|
||||
description: string
|
||||
imageUrl: string
|
||||
githubUrl: string
|
||||
hours: number
|
||||
hackatimeUrl: string
|
||||
}
|
||||
|
||||
interface NewsItem {
|
||||
id: number
|
||||
date: string
|
||||
content: string
|
||||
}
|
||||
|
||||
let projects = $state<Project[]>([])
|
||||
let news = $state<NewsItem[]>([])
|
||||
let selectedProject = $state<Project | null>(null)
|
||||
let screws = $state(42)
|
||||
|
||||
const dummyProjects: Project[] = [
|
||||
{
|
||||
id: 1,
|
||||
userId: 1,
|
||||
name: 'Blueprint',
|
||||
description: 'A hackathon project for AMD',
|
||||
imageUrl: '/hero.png',
|
||||
githubUrl: 'https://github.com/hackclub/blueprint',
|
||||
hours: 24,
|
||||
hackatimeUrl: 'https://hackatime.hackclub.com/projects/blueprint'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
userId: 1,
|
||||
name: 'Flavortown',
|
||||
description: 'A food discovery app',
|
||||
imageUrl: '/hero.png',
|
||||
githubUrl: 'https://github.com/hackclub/flavortown',
|
||||
hours: 18,
|
||||
hackatimeUrl: 'https://hackatime.hackclub.com/projects/flavortown'
|
||||
}
|
||||
]
|
||||
|
||||
const dummyNews: NewsItem[] = [
|
||||
{
|
||||
id: 1,
|
||||
date: 'jan 21, 2026',
|
||||
content: 'remember to stay drafty!'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
date: 'jan 15, 2026',
|
||||
content: 'new items added to the shop!'
|
||||
}
|
||||
]
|
||||
|
||||
onMount(async () => {
|
||||
// TODO: Replace with actual API call to /api/projects
|
||||
// const response = await fetch('/api/projects', {
|
||||
// headers: { 'Authorization': `Bearer ${userToken}` }
|
||||
// })
|
||||
// projects = await response.json()
|
||||
projects = dummyProjects
|
||||
|
||||
// TODO: Replace with actual API call to /api/news
|
||||
// const newsResponse = await fetch('/api/news')
|
||||
// news = await newsResponse.json()
|
||||
news = dummyNews
|
||||
|
||||
// TODO: Fetch user's screw count
|
||||
// const userResponse = await fetch('/api/user')
|
||||
// screws = (await userResponse.json()).screws
|
||||
})
|
||||
|
||||
function openEditModal(project: Project) {
|
||||
selectedProject = project
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
selectedProject = null
|
||||
}
|
||||
|
||||
function handleSaveProject(updatedProject: Project) {
|
||||
// TODO: Call API to update project
|
||||
// await fetch(`/api/projects/${updatedProject.id}`, {
|
||||
// method: 'PUT',
|
||||
// body: JSON.stringify(updatedProject)
|
||||
// })
|
||||
projects = projects.map((p) => (p.id === updatedProject.id ? updatedProject : p))
|
||||
closeModal()
|
||||
}
|
||||
|
||||
function createNewProject() {
|
||||
// TODO: Navigate to project creation or open modal
|
||||
console.log('Create new project')
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>dashboard | scraps</title>
|
||||
</svelte:head>
|
||||
|
||||
<DashboardNavbar {screws} />
|
||||
|
||||
<div class="pt-24 px-6 md:px-12 max-w-6xl mx-auto pb-24">
|
||||
<!-- Projects Section -->
|
||||
<div class="mb-12">
|
||||
<div class="flex gap-6 overflow-x-auto pb-4 scrollbar-hide">
|
||||
{#each projects as project (project.id)}
|
||||
<button
|
||||
onclick={() => openEditModal(project)}
|
||||
class="shrink-0 w-80 h-64 rounded-2xl border-4 border-black overflow-hidden relative group bg-[#1a365d] cursor-pointer transition-all hover:border-dashed"
|
||||
>
|
||||
<div class="absolute inset-0 flex items-center justify-center p-6">
|
||||
<img
|
||||
src={project.imageUrl}
|
||||
alt={project.name}
|
||||
class="max-w-full max-h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-2 px-4 py-2 border-2 border-dashed border-black/50 rounded-full bg-white/90 opacity-0 group-hover:opacity-100 transition-opacity"
|
||||
>
|
||||
<Pencil size={16} />
|
||||
<span class="font-bold">edit draft</span>
|
||||
</div>
|
||||
</button>
|
||||
{/each}
|
||||
|
||||
<!-- New Draft Card -->
|
||||
<button
|
||||
onclick={createNewProject}
|
||||
class="shrink-0 w-80 h-64 rounded-2xl border-4 border-black flex flex-col items-center justify-center gap-4 cursor-pointer transition-all hover:border-dashed bg-white"
|
||||
>
|
||||
<FilePlus2 size={64} strokeWidth={1.5} />
|
||||
<span class="text-2xl font-bold">new draft</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- News Section -->
|
||||
<div class="mb-16">
|
||||
{#each news as item (item.id)}
|
||||
<div class="border-4 border-black rounded-2xl p-8 text-center mb-4">
|
||||
<p class="text-lg font-bold mb-2">{item.date}</p>
|
||||
<p class="text-2xl md:text-3xl font-bold">{item.content}</p>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<!-- Shop Sections -->
|
||||
<div class="mb-16">
|
||||
<h2 class="text-4xl font-bold mb-6">shop</h2>
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div class="border-4 border-black rounded-2xl p-6">
|
||||
<h3 class="text-2xl font-bold mb-2">items</h3>
|
||||
<p class="text-gray-600">browse available scraps</p>
|
||||
</div>
|
||||
<div class="border-4 border-black rounded-2xl p-6">
|
||||
<h3 class="text-2xl font-bold mb-2">refinery</h3>
|
||||
<p class="text-gray-600">upgrade your scraps</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer Branding -->
|
||||
<div class="mt-16">
|
||||
<h1 class="text-6xl md:text-8xl font-bold mb-2">scraps</h1>
|
||||
<p class="text-xl md:text-2xl mb-4">
|
||||
<RandomPhrase />
|
||||
</p>
|
||||
<p class="text-sm text-gray-600">made with <3 by hack club</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ProjectModal project={selectedProject} onClose={closeModal} onSave={handleSaveProject} />
|
||||
|
||||
<style>
|
||||
.scrollbar-hide {
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
.scrollbar-hide::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
Loading…
Add table
Reference in a new issue