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
41 changes: 41 additions & 0 deletions app/api/account/refresh-media/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { NextRequest, NextResponse } from "next/server";
import { resolveAccountId, unauthorized } from "@/lib/api";
import { getAccountById, updateAccountConfig } from "@/lib/db";
import { provisionTenant } from "@/lib/provision";
import { listRepoAssets } from "@/lib/github";

export const runtime = "nodejs";
export const dynamic = "force-dynamic";

// Re-pull media (images / video / audio) from the account's connected GitHub
// repo and refresh config.assets — without touching any other config field.
export async function POST(req: NextRequest) {
const id = await resolveAccountId(req);
if (!id) return unauthorized();

const acct = await getAccountById(id);
if (!acct) return NextResponse.json({ error: "account not found" }, { status: 404 });

const config: Record<string, any> = { ...(acct.config || {}) };
if (!config.repo) {
return NextResponse.json({ error: "Connect a GitHub repo first (owner/name), then Save." }, { status: 400 });
}

let assets: Awaited<ReturnType<typeof listRepoAssets>>;
try {
assets = await listRepoAssets(config.repo, { pattern: config.assetPattern });
} catch (e: any) {
return NextResponse.json({ error: `Couldn't load from ${config.repo}: ${e?.message || e}` }, { status: 502 });
}

config.assets = assets;
const updated = await updateAccountConfig(id, config);
if (updated?.status === "active") await provisionTenant(updated);

const counts: Record<string, number> = {};
for (const x of assets) {
const k = x.kind || "image";
counts[k] = (counts[k] || 0) + 1;
}
return NextResponse.json({ ok: true, total: assets.length, counts, assets });
}
29 changes: 24 additions & 5 deletions app/dashboard/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,8 @@ function AccountPanel({ onError, onOk }: { onError: (m: string) => void; onOk: (
const [wallet, setWallet] = useState("");
const [repo, setRepo] = useState("");
const [assetPattern, setAssetPattern] = useState("");
const [assets, setAssets] = useState<{ label: string; url: string }[]>([]);
const [assets, setAssets] = useState<{ label: string; url: string; kind?: string }[]>([]);
const [refreshing, setRefreshing] = useState(false);
const [videos, setVideos] = useState<{ name: string; url: string }[]>([]);
const [uploading, setUploading] = useState(false);
const [blocks, setBlocks] = useState<BlockRow[]>([]);
Expand Down Expand Up @@ -439,15 +440,33 @@ function AccountPanel({ onError, onOk }: { onError: (m: string) => void; onOk: (
))}
</ul>

<h3 className="ed-h">GitHub assets <span className="muted">(pull images from a repo onto your page)</span></h3>
<h3 className="ed-h">GitHub media <span className="muted">(pull images, video &amp; audio from a repo onto your page)</span></h3>
<div className="row"><input className="inp" placeholder="owner/repo — e.g. moshcoder/moshcoding" value={repo} onChange={(e) => setRepo(e.target.value)} /></div>
<div className="row"><input className="inp" placeholder="path glob — e.g. images/*_thumb.png" value={assetPattern} onChange={(e) => setAssetPattern(e.target.value)} /></div>
<div className="row">
<input className="inp" placeholder="path glob — e.g. media/* (blank = all media)" value={assetPattern} onChange={(e) => setAssetPattern(e.target.value)} />
<button className="btn2 ghost" type="button" disabled={refreshing || !repo.trim()} onClick={async () => {
setRefreshing(true);
try {
const r = await fetch("/api/account/refresh-media", { method: "POST" });
const d = await r.json();
if (!r.ok) throw new Error(d.error || "Refresh failed.");
setAssets(d.assets || []);
const c = d.counts || {};
onOk(`Refreshed ${d.total} — ${c.image || 0} image, ${c.video || 0} video, ${c.audio || 0} audio. 🤘`);
} catch (e: any) { onError(e.message || "Refresh failed."); }
finally { setRefreshing(false); }
}}>{refreshing ? "Refreshing…" : "↻ Refresh media"}</button>
</div>
{assets.length > 0 && (
<div className="t-assets" style={{ margin: "10px 0 0" }}>
{assets.slice(0, 12).map((a, i) => <span key={i} className="t-asset"><img src={a.url} alt={a.label} loading="lazy" /></span>)}
{assets.slice(0, 12).map((a: any, i: number) =>
a.kind === "video" ? <span key={i} className="t-asset t-asset-chip">🎬 {a.label || "video"}</span>
: a.kind === "audio" ? <span key={i} className="t-asset t-asset-chip">🎧 {a.label || "audio"}</span>
: <span key={i} className="t-asset"><img src={a.url} alt={a.label} loading="lazy" /></span>,
)}
</div>
)}
{repo && <p className="sub" style={{ marginTop: 6 }}>{assets.length} asset(s) loaded. Public repos work as-is; private repos need a server GITHUB_TOKEN.</p>}
{repo && <p className="sub" style={{ marginTop: 6 }}>{assets.length} asset(s) loaded. Public repos work as-is; private repos need a server GITHUB_TOKEN. Use <b>↻ Refresh media</b> to re-pull without saving the rest of the form.</p>}

<div className="row" style={{ marginTop: 16 }}>
<button className="btn2" disabled={saving} onClick={save}>{saving ? "Saving…" : `Save & publish ${activeDomain}`}</button>
Expand Down
6 changes: 6 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,12 @@ dialog.qhelp::backdrop { background: rgba(0,0,0,.72); backdrop-filter: blur(2px)
.t-asset { display: grid; place-items: center; aspect-ratio: 1/1; padding: 8px; background: #111214; border: 1px solid #262629; border-radius: 10px; overflow: hidden; transition: border-color .15s ease; }
.t-asset:hover { border-color: var(--tenant-accent, var(--acid)); }
.t-asset img { max-width: 100%; max-height: 100%; object-fit: contain; }
/* mixed-media repo assets: video/audio tiles + dashboard chips */
.t-asset-av { aspect-ratio: 1/1; width: 100%; padding: 0; background: #000; }
.t-asset-av video, video.t-asset-av { width: 100%; height: 100%; object-fit: cover; }
.t-asset-audio { grid-column: 1 / -1; aspect-ratio: auto; width: 100%; padding: 8px; }
.t-asset-audio audio, audio.t-asset-audio { width: 100%; }
.t-asset-chip { aspect-ratio: auto; padding: 8px 10px; font-family: var(--mono); font-size: 11px; color: var(--ash); text-align: center; word-break: break-word; }

/* Dashboard tabs */
.tabs { display: flex; gap: 8px; margin: 4px 0 18px; border-bottom: 1px solid var(--line); }
Expand Down
16 changes: 11 additions & 5 deletions components/Tenant.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,11 +98,17 @@ export default function Tenant({ cfg }: { cfg: TenantConfig }) {

{cfg.assets.length > 0 && (
<div className="t-assets" aria-label="Assets">
{cfg.assets.map((a, i) => (
<a key={i} className="t-asset" href={a.url} target="_blank" rel="noopener noreferrer" title={a.label}>
<img src={a.url} alt={a.label} loading="lazy" />
</a>
))}
{cfg.assets.map((a, i) =>
a.kind === "video" ? (
<video key={i} className="t-asset t-asset-av" controls preload="metadata" playsInline src={a.url} title={a.label} />
) : a.kind === "audio" ? (
<audio key={i} className="t-asset t-asset-audio" controls preload="none" src={a.url} title={a.label} />
) : (
<a key={i} className="t-asset" href={a.url} target="_blank" rel="noopener noreferrer" title={a.label}>
<img src={a.url} alt={a.label} loading="lazy" />
</a>
),
)}
</div>
)}

Expand Down
9 changes: 7 additions & 2 deletions lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,9 +394,14 @@ export function configFor(dn: string, opts: TenantOverrides = {}): TenantConfig
const links = [...socialLinks(dn, override), ...parseLinks(opts.linkParams), ...cleanLinks(ov.customLinks, "link")];
// Sponsors: ?aff_linkN= query + saved sponsors.
const sponsors = [...parseSponsors(opts.affParams), ...cleanLinks(ov.sponsors, "sponsor")];
// GitHub repo image assets (saved with {name,url}); rendered as a gallery.
// GitHub repo media assets (images/video/audio, {name,url,kind}); a gallery.
const assetKinds = new Set(["image", "video", "audio"]);
const assets = Array.isArray(ov.assets)
? ov.assets.filter((a: any) => a && a.url).map((a: any) => ({ label: String(a.name || a.label || ""), url: String(a.url), kind: "image" }))
? ov.assets.filter((a: any) => a && a.url).map((a: any) => ({
label: String(a.name || a.label || ""),
url: String(a.url),
kind: assetKinds.has(a.kind) ? String(a.kind) : "image",
}))
: [];
// Uploaded MP4 videos ({name, url, poster?}).
const videos = Array.isArray(ov.videos)
Expand Down
Binary file modified lib/github.ts
Binary file not shown.
Loading