Add persisted and shareable builds
This commit is contained in:
@@ -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 };
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
};
|
||||
};
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user