Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 16 additions & 26 deletions ui/index.html
Original file line number Diff line number Diff line change
@@ -1,52 +1,42 @@
<!DOCTYPE html>
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, viewport-fit=cover"
/>
<meta name="color-scheme" content="light dark" />
<meta name="format-detection" content="telephone=no" />

<!-- Favicon -->
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/manifest.json" />

<!-- Preconnect to font origins -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />

<!-- Theme -->
<meta name="theme-color" content="#171717" />

<!-- Theme flash prevention - runs before any render -->
<script>
(function() {
var t = localStorage.getItem('theme');
if (t === 'dark' || (!t && window.matchMedia('(prefers-color-scheme: dark)').matches)) {
document.documentElement.classList.add('dark');
(function () {
var t = localStorage.getItem("theme");
if (
t === "dark" ||
(!t && window.matchMedia("(prefers-color-scheme: dark)").matches)
) {
document.documentElement.classList.add("dark");
}
})();
</script>

<style>
/* Minimal shell reset - matches host for dev consistency */
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { height: 100%; -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
body {
min-height: 100%;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
#app { min-height: 100vh; }
@supports (min-height: 100dvh) {
#app { min-height: 100dvh; }
}
</style>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
8 changes: 7 additions & 1 deletion ui/rsbuild.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,12 @@ export default defineConfig({
"Access-Control-Allow-Methods": "GET, POST, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type",
},
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
},
},
tools: {
rspack: {
Expand Down Expand Up @@ -174,7 +180,7 @@ export default defineConfig({
// ? `${bosConfig.app.ui.production}/`
// : "auto",
filename: {
css: "static/css/[name].css",
css: "[name].css",
},
copy: [
{
Expand Down
174 changes: 174 additions & 0 deletions ui/src/components/builders/BuilderDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
/**
* Builder Details Component
* Right panel showing selected builder info
*/

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import type { Builder } from "./BuilderListItem";

interface BuilderDetailsProps {
builder: Builder;
}

export function BuilderDetails({ builder }: BuilderDetailsProps) {
return (
<div className="flex-1 border border-primary/30 bg-background h-full overflow-y-auto">
<div className="p-4 sm:p-6 space-y-6">
{/* Header */}
<BuilderHeader builder={builder} />

{/* Skills */}
<BuilderSkills tags={builder.tags} />

{/* About */}
<BuilderAbout description={builder.description} />

{/* Projects */}
<BuilderProjects projects={builder.projects} />

{/* Socials */}
{builder.socials && <BuilderSocials socials={builder.socials} />}
</div>
</div>
);
}

function BuilderHeader({ builder }: { builder: Builder }) {
return (
<div className="flex items-start gap-4">
<Avatar className="size-16 sm:size-14 border-2 border-primary/60">
<AvatarImage src={builder.avatar || undefined} />
<AvatarFallback className="bg-primary/20 text-primary text-lg sm:text-base font-mono font-bold">
{builder.displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="space-y-2">
<h2 className="text-xl sm:text-xl font-bold text-foreground">
{builder.displayName}
</h2>
<p className="font-mono text-primary text-sm sm:text-sm">{builder.accountId}</p>
<span className="inline-block text-xs bg-primary/25 text-primary px-3 py-1.5 font-mono font-medium">
{builder.role}
</span>
</div>
</div>
);
}

function BuilderSkills({ tags }: { tags: string[] }) {
return (
<div className="space-y-3">
<h3 className="text-sm text-muted-foreground font-mono uppercase tracking-wider">
Skills
</h3>
<div className="flex flex-wrap gap-2">
{tags.map((tag) => (
<span
key={tag}
className="text-sm bg-muted/60 text-foreground px-3 py-1.5 border border-border/50"
>
{tag}
</span>
))}
</div>
</div>
);
}

function BuilderAbout({ description }: { description: string }) {
return (
<div className="space-y-3">
<h3 className="text-sm text-muted-foreground font-mono uppercase tracking-wider">
About
</h3>
<p className="text-foreground text-base leading-relaxed">{description}</p>
</div>
);
}

function BuilderProjects({
projects,
}: {
projects: { name: string; description: string; status: string }[];
}) {
return (
<div className="space-y-3">
<h3 className="text-sm text-muted-foreground font-mono uppercase tracking-wider">
Building
</h3>
<div className="space-y-3">
{projects.map((project) => (
<div
key={project.name}
className="p-4 border border-border/50 bg-muted/30 space-y-2"
>
<div className="flex items-center justify-between gap-2">
<span className="font-mono text-foreground font-semibold text-base">
{project.name}
</span>
<ProjectStatus status={project.status} />
</div>
<p className="text-sm text-muted-foreground">
{project.description}
</p>
</div>
))}
</div>
</div>
);
}

function ProjectStatus({ status }: { status: string }) {
const statusClass =
status === "Active"
? "bg-primary/30 text-primary border-primary/40"
: status === "In Development"
? "bg-accent/30 text-accent border-accent/40"
: status === "Beta"
? "bg-blue-500/30 text-blue-400 border-blue-500/40"
: "bg-muted text-muted-foreground border-border";

return (
<span className={`text-[10px] px-2 py-0.5 font-mono font-medium border ${statusClass}`}>
{status}
</span>
);
}

function BuilderSocials({
socials,
}: {
socials: { github?: string; twitter?: string };
}) {
if (!socials.github && !socials.twitter) return null;

return (
<div className="space-y-3">
<h3 className="text-sm text-muted-foreground font-mono uppercase tracking-wider">
Connect
</h3>
<div className="flex flex-wrap gap-4">
{socials.github && (
<a
href={`https://github.com/${socials.github}`}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary hover:text-primary/80 transition-colors font-mono underline underline-offset-4"
>
github/{socials.github}
</a>
)}
{socials.twitter && (
<a
href={`https://twitter.com/${socials.twitter}`}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-primary hover:text-primary/80 transition-colors font-mono underline underline-offset-4"
>
@{socials.twitter}
</a>
)}
</div>
</div>
);
}
36 changes: 36 additions & 0 deletions ui/src/components/builders/BuilderList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Builder List Component
* Left panel showing all builders
*/

import { BuilderListItem, type Builder } from "./BuilderListItem";

interface BuilderListProps {
builders: Builder[];
selectedId: string;
onSelect: (builder: Builder) => void;
}

export function BuilderList({ builders, selectedId, onSelect }: BuilderListProps) {
return (
<div className="w-full lg:w-[350px] shrink-0 border border-primary/30 bg-background flex flex-col h-full">
<div className="px-4 py-3 border-b border-primary/20 bg-primary/5 shrink-0">
<span className="text-sm text-primary font-mono uppercase tracking-wider font-medium">
{builders.length} Legionnaires
</span>
</div>
<div className="flex-1 overflow-y-auto">
<div className="divide-y divide-border/40">
{builders.map((builder) => (
<BuilderListItem
key={builder.id}
builder={builder}
isSelected={selectedId === builder.id}
onSelect={() => onSelect(builder)}
/>
))}
</div>
</div>
</div>
);
}
65 changes: 65 additions & 0 deletions ui/src/components/builders/BuilderListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/**
* Builder List Item Component
* Single builder entry in the list
*/

import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";

export interface Builder {
id: string;
accountId: string;
displayName: string;
avatar: string | null;
role: string;
tags: string[];
description: string;
projects: {
name: string;
description: string;
status: string;
}[];
socials: {
github?: string;
twitter?: string;
};
}

interface BuilderListItemProps {
builder: Builder;
isSelected: boolean;
onSelect: () => void;
}

export function BuilderListItem({ builder, isSelected, onSelect }: BuilderListItemProps) {
return (
<button
type="button"
onClick={onSelect}
className={`w-full text-left px-4 py-4 transition-colors hover:bg-primary/10 ${
isSelected
? "bg-primary/15 border-l-3 border-l-primary"
: "hover:border-l-3 hover:border-l-primary/50"
}`}
>
<div className="flex gap-4 items-center">
<Avatar className="size-12 border-2 border-primary/40">
<AvatarImage src={builder.avatar || undefined} />
<AvatarFallback className="bg-primary/10 text-primary text-sm font-mono font-bold">
{builder.displayName.slice(0, 2).toUpperCase()}
</AvatarFallback>
</Avatar>
<div className="flex-1 min-w-0 space-y-1.5">
<span className="font-mono text-base text-foreground font-medium truncate block">
{builder.accountId}
</span>
<div className="flex flex-wrap gap-1">
<span className="text-xs text-primary/80 bg-primary/10 px-2 py-0.5 border border-primary/20">
{builder.role}
</span>
</div>
</div>
<span className="text-muted-foreground/50 text-xl">›</span>
</div>
</button>
);
}
3 changes: 3 additions & 0 deletions ui/src/components/builders/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export { BuilderList } from "./BuilderList";
export { BuilderDetails } from "./BuilderDetails";
export { BuilderListItem, type Builder } from "./BuilderListItem";
Loading
Loading