feat: explore page space banners, icon-derived gradients, and space descriptions

- Redesign explore cards with banner images, overlapping icons, and frosted fade
- Extract dominant colors from space icons for dynamic banner gradients
- Add space description/banner fields to schema with migration
- Move origin label from banner overlay to content metadata row
- Support space descriptions in settings overview panel
This commit is contained in:
Jannis Braun
2026-03-09 13:49:50 +01:00
parent fccfea8b2e
commit 22e7616c70
13 changed files with 485 additions and 46 deletions
+8 -2
View File
@@ -25,6 +25,7 @@ function rowToSpace(row: typeof schema.spaces.$inferSelect): Space {
id: row.id,
name: row.name,
icon: row.icon,
banner: row.banner ?? null,
ownerId: row.ownerId,
inviteCode: row.inviteCode,
visibility: (row.visibility ?? 'private') as Space['visibility'],
@@ -54,7 +55,7 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
app.post<{ Body: CreateSpaceRequest }>('/api/spaces', {
preHandler: authenticate,
}, async (request, reply) => {
const { name, icon, visibility, description } = request.body;
const { name, icon, banner, visibility, description } = request.body;
if (!name || typeof name !== 'string') {
return reply.code(400).send({ error: 'Space name is required', statusCode: 400 });
@@ -84,6 +85,7 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
id: spaceId,
name: trimmedName,
icon: icon ?? null,
banner: banner ?? null,
ownerId: request.userId,
inviteCode,
visibility: safeVisibility,
@@ -269,7 +271,7 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
preHandler: authenticate,
}, async (request, reply) => {
const { id } = request.params;
const { name, icon, visibility, description } = request.body;
const { name, icon, banner, visibility, description } = request.body;
const db = getDb();
const server = db.select().from(schema.spaces).where(eq(schema.spaces.id, id)).get();
@@ -295,6 +297,10 @@ export async function spaceRoutes(app: FastifyInstance): Promise<void> {
updates.icon = icon || null;
}
if (banner !== undefined) {
updates.banner = banner || null;
}
if (visibility !== undefined) {
const validVisibilities = ['public', 'request', 'private'];
if (!validVisibilities.includes(visibility)) {