Add persisted and shareable builds

This commit is contained in:
JB
2026-07-25 22:28:40 -04:00
parent 1c32b1dfdc
commit 7bb4b92965
11 changed files with 263 additions and 14 deletions
+20
View File
@@ -0,0 +1,20 @@
import { getDatabase } from '$lib/server/database';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async () => {
const database = await getDatabase();
const builds = await (await database.prepare(`
SELECT builds.id,builds.title,builds.slug,builds.updated_at,
COALESCE(user_profiles.display_name,users.username,'Deleted Agent') AS author,
COALESCE(AVG(ratings.value),0) AS rating,COUNT(ratings.value) AS votes
FROM builds
JOIN users ON users.id=builds.owner_id
LEFT JOIN user_profiles ON user_profiles.user_id=users.id
LEFT JOIN ratings ON ratings.build_id=builds.id
WHERE builds.visibility='public' AND builds.loadout_json IS NOT NULL
GROUP BY builds.id
ORDER BY builds.updated_at DESC
LIMIT 60
`)).all();
return { builds };
};
+19 -6
View File
@@ -1,9 +1,22 @@
<script lang="ts">
const builds = [
{ title: 'Striker Critical Rush', author: 'AgentNova', mode: 'PVE', rating: 4.9, votes:128, tags:['AR','CRITICAL','SOLO'], color:'#f6a821' },
{ title: 'Ironclad Vanguard', author: 'KelsoMain', mode: 'PVP', rating: 4.8, votes:94, tags:['TANK','SHIELD','TEAM'], color:'#61b7d7' },
{ title: 'Status Protocol', author: 'SHD_Warden', mode: 'PVE', rating: 4.7, votes:76, tags:['SKILL','STATUS','SUPPORT'], color:'#d36b5c' }
];
let { data } = $props();
</script>
<svelte:head><title>Discover builds — SHD.build</title></svelte:head>
<main class="page catalogue-page"><div class="catalogue-hero"><div><p class="eyebrow"><span></span> SHD LOADOUT ARCHIVE</p><h1>DISCOVER<br/><em>BUILDS.</em></h1><p>Field-tested configurations submitted by agents across Manhattan.</p></div><div class="catalogue-count"><strong>2,847</strong><span>PUBLIC LOADOUTS</span><a class="primary" href="/builder">NEW BUILD +</a></div></div><div class="filter-bar"><label>SEARCH ARCHIVE<input placeholder="Build name, weapon, agent…" /></label><label>ACTIVITY<select><option>ALL ACTIVITIES</option><option>PVE</option><option>PVP</option></select></label><label>SORT BY<select><option>TOP RATED</option><option>MOST RECENT</option><option>MOST SAVED</option></select></label><button class="secondary">FILTERS +</button></div><section class="build-grid catalogue-grid">{#each builds as build}<article style={`--build-color:${build.color}`}><div class="card-top"><span>{build.mode}</span><span>★ {build.rating}</span></div><div class="weapon-art"><div></div><span>SHD LOADOUT</span></div><div class="card-body"><p>COMMUNITY BUILD</p><h3>{build.title}</h3><div class="tags">{#each build.tags as tag}<span>{tag}</span>{/each}</div><div class="author"><span class="avatar">{build.author.slice(0,1)}</span><span>BY <strong>{build.author}</strong></span><span>{build.votes} RATINGS</span></div><button class="build-open">VIEW LOADOUT →</button></div></article>{/each}</section></main>
<main class="page catalogue-page">
<div class="catalogue-hero">
<div><p class="eyebrow"><span></span> SHD LOADOUT ARCHIVE</p><h1>DISCOVER<br/><em>BUILDS.</em></h1><p>Field-tested configurations submitted by registered agents.</p></div>
<div class="catalogue-count"><strong>{data.builds.length}</strong><span>PUBLIC LOADOUTS</span><a class="primary" href="/builder">NEW BUILD +</a></div>
</div>
<section class="build-grid catalogue-grid">
{#each data.builds as build}
<article style="--build-color:#f6a821">
<div class="card-top"><span>PUBLIC</span><span>{Number(build.votes) ? `★ ${Number(build.rating).toFixed(1)}` : 'UNRATED'}</span></div>
<div class="weapon-art"><div></div><span>SHD LOADOUT</span></div>
<div class="card-body"><p>COMMUNITY BUILD</p><h3>{build.title}</h3><div class="author"><span class="avatar">{String(build.author).slice(0,1)}</span><span>BY <strong>{build.author}</strong></span><span>{build.votes} RATINGS</span></div><a class="build-open" href={`/builds/${build.slug}`}>VIEW LOADOUT →</a></div>
</article>
{:else}
<div class="catalogue-empty"><span>◇</span><strong>NO PUBLIC BUILDS YET</strong><p>Be the first agent to publish a loadout to the archive.</p><a class="primary" href="/builder">CREATE BUILD →</a></div>
{/each}
</section>
</main>
+34
View File
@@ -0,0 +1,34 @@
import { error } from '@sveltejs/kit';
import { getDatabase } from '$lib/server/database';
import { getSessionUser } from '$lib/server/auth/session';
import type { PageServerLoad } from './$types';
export const load: PageServerLoad = async ({ params, cookies }) => {
const database = await getDatabase();
const viewer = await getSessionUser(cookies);
const build = await (await database.prepare(`
SELECT builds.*,COALESCE(user_profiles.display_name,users.username,'Deleted Agent') AS author
FROM builds
JOIN users ON users.id=builds.owner_id
LEFT JOIN user_profiles ON user_profiles.user_id=users.id
WHERE builds.slug=?
`)).get(params.slug);
if (!build || (build.visibility === 'private' && build.owner_id !== viewer?.id)) error(404, 'Build not found');
let loadout;
try { loadout = JSON.parse(String(build.loadout_json)); } catch { error(404, 'Build data is unavailable'); }
const [gear, weapons, chips, protocols, brands] = await Promise.all([
(await database.prepare('SELECT id,name,slot,tier FROM gear')).all(),
(await database.prepare('SELECT id,name,category,is_exotic FROM weapons')).all(),
(await database.prepare('SELECT id,name,quality FROM skill_chip_sets')).all(),
(await database.prepare('SELECT id,name,quality FROM os_protocols')).all(),
(await database.prepare('SELECT id,name FROM brand_sets')).all()
]);
return {
build: {
id: String(build.id), title: String(build.title), slug: String(build.slug),
visibility: String(build.visibility), author: String(build.author),
owner: build.owner_id === viewer?.id, updatedAt: String(build.updated_at)
},
loadout, gear, weapons, chips, protocols, brands
};
};
+32
View File
@@ -0,0 +1,32 @@
<script lang="ts">
import { gearSlots } from '$lib/builder/loadout';
let { data } = $props();
const gear = (id: string) => data.gear.find((item) => item.id === id);
const weapon = (id: string) => data.weapons.find((item) => item.id === id);
const chip = (id: string) => data.chips.find((item) => item.id === id);
const protocol = (id: string) => data.protocols.find((item) => item.id === id);
const brand = (id: string) => data.brands.find((item) => item.id === id);
</script>
<svelte:head><title>{data.build.title} — SHD.build</title></svelte:head>
<main class="app-page shared-build-page">
<header class="shared-build-command">
<div><p class="eyebrow"><span></span> SHD LOADOUT ARCHIVE / {data.build.visibility.toUpperCase()}</p><h1>{data.build.title}</h1><p>Filed by <strong>{data.build.author}</strong> · Updated {new Date(data.build.updatedAt).toLocaleDateString()}</p></div>
<div>{#if data.build.owner}<a class="secondary" href={`/builder?build=${data.build.slug}`}>EDIT LOADOUT</a>{/if}<a class="primary" href="/builder">NEW BUILD +</a></div>
</header>
<div class="shared-build-grid">
<section>
<header><span>WEAPONS</span><b>02 SLOTS</b></header>
{#each data.loadout.weapons as selection, index}{@const item=weapon(selection.itemId)}<article><small>{index ? 'SECONDARY' : 'PRIMARY'}</small><strong>{item?.name ?? 'EMPTY SLOT'}</strong><span>{item ? `${item.category} · T${selection.tier} · LEVEL ${selection.level}` : 'NOT CONFIGURED'}</span></article>{/each}
</section>
<section>
<header><span>GEAR</span><b>06 SLOTS</b></header>
{#each gearSlots as slot}{@const selection=data.loadout.gear[slot]}{@const item=gear(selection.itemId)}<article><small>{slot.toUpperCase()}</small><strong>{item?.name ?? 'EMPTY SLOT'}</strong><span>{item ? `${brand(selection.brandId)?.name ?? 'NO BRAND'} · T${item.tier} · LEVEL ${selection.level}` : 'NOT CONFIGURED'}</span></article>{/each}
</section>
<section>
<header><span>AGENT SYSTEMS</span><b>04 SLOTS</b></header>
{#each data.loadout.chips as selection, index}{@const item=chip(selection.setId)}<article><small>SKILL CHIP 0{index+1}</small><strong>{item?.name ?? 'EMPTY SLOT'}</strong><span>{item ? selection.quality : 'NOT CONFIGURED'}</span></article>{/each}
{#if protocol(data.loadout.protocol.itemId)}{@const protocolItem=protocol(data.loadout.protocol.itemId)}<article><small>OS PROTOCOL</small><strong>{protocolItem.name}</strong><span>LEVEL {data.loadout.protocol.level} · {data.loadout.protocol.talentUpgrades}/5 UPGRADES</span></article>{:else}<article><small>OS PROTOCOL</small><strong>EMPTY SLOT</strong><span>NOT CONFIGURED</span></article>{/if}
</section>
</div>
</main>