Files
Sahaj Jain 249b72f98a Community themes (#259)
* add community themes feature

Allow users to publish saved themes to a public community page,
browse/sort community themes, and like them with auth-gated interactions.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* improve community themes UI and fix like state

- Polish community page with better layout, sort icons, loading skeletons
- Show author, date, like count on theme detail page
- Fix isLikedByMe not being fetched in getCommunityDataForTheme
- Remove Community link from app header
- Replace Save with Publish button in editor action bar for saved themes
- Include isPublished in getThemes() via server-side join to avoid layout shift

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* ui

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 01:32:17 +05:30

90 lines
2.1 KiB
TypeScript

import z from "zod";
// Error codes for server actions - these survive serialization
export const ErrorCode = {
UNAUTHORIZED: "UNAUTHORIZED",
VALIDATION_ERROR: "VALIDATION_ERROR",
THEME_NOT_FOUND: "THEME_NOT_FOUND",
THEME_LIMIT_REACHED: "THEME_LIMIT_REACHED",
ALREADY_PUBLISHED: "ALREADY_PUBLISHED",
UNKNOWN_ERROR: "UNKNOWN_ERROR",
} as const;
export type ErrorCode = (typeof ErrorCode)[keyof typeof ErrorCode];
// Typed result for server actions
export type ActionResult<T> =
| { success: true; data: T }
| { success: false; error: { code: ErrorCode; message: string } };
// Helper to create error results
export function actionError(code: ErrorCode, message: string): ActionResult<never> {
return { success: false, error: { code, message } };
}
// Helper to create success results
export function actionSuccess<T>(data: T): ActionResult<T> {
return { success: true, data };
}
export class UnauthorizedError extends Error {
constructor(message = "Unauthorized") {
super(message);
this.name = "UnauthorizedError";
}
}
export class ValidationError extends Error {
constructor(
message: string,
public details?: unknown
) {
super(message);
this.name = "ValidationError";
}
}
export class SubscriptionRequiredError extends Error {
constructor(
message = "Subscription required",
public data?: unknown
) {
super(message);
this.name = "SubscriptionRequiredError";
}
}
export class ThemeNotFoundError extends Error {
constructor(message = "Theme not found") {
super(message);
this.name = "ThemeNotFoundError";
}
}
export type ApiErrorCode =
| "SUBSCRIPTION_REQUIRED"
| "VALIDATION_ERROR"
| "UNAUTHORIZED"
| "UNKNOWN_ERROR";
export class ApiError extends Error {
constructor(
public code: ApiErrorCode,
message: string,
public data?: unknown,
public status?: number
) {
super(message);
this.name = "ApiError";
}
}
export const MyErrorResponseSchema = z.object({
code: z.string().optional(),
message: z.string().optional(),
data: z.unknown().optional(),
status: z.number().optional(),
});
export type MyErrorResponseType = z.infer<typeof MyErrorResponseSchema>;