feat: add usernames, pretty URLs, cloning, and sharing docs
## Usernames
- Add username field to User model (unique, URL-friendly)
- Add slug field to Collection model (unique per user)
- Update sign-up flow to require username with availability checking
- Create username check API endpoint
## Pretty URLs
- Add route group (profile) with pretty URL pages:
- /{username} - User profile
- /{username}/agents/{uid} - Agent detail
- /{username}/agents/{uid}/chat - Chat redirect
- /{username}/collections/{slug} - Collection detail
- Add client-side redirects from old /agents/[id] and /collections/[id] URLs
## Cloning
- Add clone API endpoints for agents and collections
- Create CloneButton component
- Add AGENT_CLONED and COLLECTION_CLONED activity types
## Documentation
- Add /docs/sharing page explaining all shareable URLs
- Document cloning functionality and visibility settings
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
1ff0e49e41
commit
eaaa40f130
28 changed files with 3061 additions and 14 deletions
|
|
@ -32,6 +32,10 @@
|
|||
"./agent": {
|
||||
"types": "./dist/agent.d.ts",
|
||||
"default": "./dist/agent.js"
|
||||
},
|
||||
"./user": {
|
||||
"types": "./dist/user.d.ts",
|
||||
"default": "./dist/user.js"
|
||||
}
|
||||
},
|
||||
"files": ["dist"],
|
||||
|
|
|
|||
|
|
@ -61,6 +61,20 @@ export const AddToolToAgentSchema = z.object({
|
|||
position: z.number().int().min(0).optional(),
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Clone Schemas
|
||||
// ============================================================================
|
||||
|
||||
export const CloneAgentSchema = z.object({
|
||||
name: z.string().min(1).max(100).optional(), // If not provided, will append "(copy)"
|
||||
uid: z
|
||||
.string()
|
||||
.min(1)
|
||||
.max(50)
|
||||
.regex(UID_REGEX, 'UID must be lowercase alphanumeric with hyphens')
|
||||
.optional(), // If not provided, will generate from name
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// User API Key Schemas
|
||||
// ============================================================================
|
||||
|
|
@ -165,6 +179,7 @@ export type CreateAgentInput = z.infer<typeof CreateAgentSchema>;
|
|||
export type UpdateAgentInput = z.infer<typeof UpdateAgentSchema>;
|
||||
export type AddCollectionToAgentInput = z.infer<typeof AddCollectionToAgentSchema>;
|
||||
export type AddToolToAgentInput = z.infer<typeof AddToolToAgentSchema>;
|
||||
export type CloneAgentInput = z.infer<typeof CloneAgentSchema>;
|
||||
export type AddApiKeyInput = z.infer<typeof AddApiKeySchema>;
|
||||
export type ApiKeyInfo = z.infer<typeof ApiKeyInfoSchema>;
|
||||
export type CreateConversationInput = z.infer<typeof CreateConversationSchema>;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,19 @@ export const ReorderToolsSchema = z.object({
|
|||
toolIds: z.array(z.string().min(1)),
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Clone Schemas
|
||||
// ============================================================================
|
||||
|
||||
export const CloneCollectionSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.min(1, 'Name is required')
|
||||
.max(100, 'Name must be 100 characters or less')
|
||||
.regex(NAME_REGEX, 'Name can only contain letters, numbers, spaces, hyphens, and underscores')
|
||||
.optional(), // If not provided, will use original name or append "(copy)"
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Response Types (for API responses)
|
||||
// ============================================================================
|
||||
|
|
@ -58,6 +71,7 @@ export const ReorderToolsSchema = z.object({
|
|||
export const CollectionSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
slug: z.string().nullable(),
|
||||
description: z.string().nullable(),
|
||||
isPublic: z.boolean(),
|
||||
toolCount: z.number(),
|
||||
|
|
@ -96,6 +110,7 @@ export type UpdateCollectionInput = z.infer<typeof UpdateCollectionSchema>;
|
|||
export type AddToolToCollectionInput = z.infer<typeof AddToolToCollectionSchema>;
|
||||
export type UpdateCollectionToolInput = z.infer<typeof UpdateCollectionToolSchema>;
|
||||
export type ReorderToolsInput = z.infer<typeof ReorderToolsSchema>;
|
||||
export type CloneCollectionInput = z.infer<typeof CloneCollectionSchema>;
|
||||
export type Collection = z.infer<typeof CollectionSchema>;
|
||||
export type CollectionTool = z.infer<typeof CollectionToolSchema>;
|
||||
export type CollectionWithTools = z.infer<typeof CollectionWithToolsSchema>;
|
||||
|
|
|
|||
146
packages/types/src/user.ts
Normal file
146
packages/types/src/user.ts
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
import { z } from 'zod';
|
||||
|
||||
// ============================================================================
|
||||
// Reserved Usernames (defined first since UsernameSchema references it)
|
||||
// ============================================================================
|
||||
|
||||
export const RESERVED_USERNAMES = [
|
||||
// System routes
|
||||
'admin',
|
||||
'api',
|
||||
'auth',
|
||||
'dashboard',
|
||||
'help',
|
||||
'support',
|
||||
'system',
|
||||
'www',
|
||||
'settings',
|
||||
'login',
|
||||
'logout',
|
||||
'register',
|
||||
'signup',
|
||||
'signin',
|
||||
// Content routes
|
||||
'agents',
|
||||
'collections',
|
||||
'tools',
|
||||
'tool',
|
||||
'playground',
|
||||
'explore',
|
||||
'search',
|
||||
// Reserved for future
|
||||
'about',
|
||||
'blog',
|
||||
'docs',
|
||||
'pricing',
|
||||
'terms',
|
||||
'privacy',
|
||||
'contact',
|
||||
'status',
|
||||
// Brand/official
|
||||
'tpmjs',
|
||||
'tpm',
|
||||
'official',
|
||||
] as const;
|
||||
|
||||
// ============================================================================
|
||||
// Username Validation
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Username requirements:
|
||||
* - 3-30 characters
|
||||
* - Lowercase alphanumeric and hyphens only
|
||||
* - Must start and end with alphanumeric (unless 1-2 chars)
|
||||
* - No consecutive hyphens
|
||||
*/
|
||||
export const USERNAME_REGEX = /^[a-z0-9](?:[a-z0-9]|(?:-(?!-))){1,28}[a-z0-9]$|^[a-z0-9]{1,2}$/;
|
||||
|
||||
export const UsernameSchema = z
|
||||
.string()
|
||||
.min(3, 'Username must be at least 3 characters')
|
||||
.max(30, 'Username must be 30 characters or less')
|
||||
.regex(USERNAME_REGEX, 'Username must be lowercase, alphanumeric, with single hyphens only')
|
||||
.refine(
|
||||
(val) => !(RESERVED_USERNAMES as readonly string[]).includes(val),
|
||||
'This username is reserved'
|
||||
);
|
||||
|
||||
// ============================================================================
|
||||
// User Schemas
|
||||
// ============================================================================
|
||||
|
||||
export const UpdateUserProfileSchema = z.object({
|
||||
name: z.string().min(1, 'Name is required').max(100).optional(),
|
||||
username: UsernameSchema.optional(),
|
||||
image: z.string().url('Invalid image URL').nullable().optional(),
|
||||
});
|
||||
|
||||
export const CheckUsernameSchema = z.object({
|
||||
username: z
|
||||
.string()
|
||||
.min(3, 'Username must be at least 3 characters')
|
||||
.max(30, 'Username must be 30 characters or less')
|
||||
.transform((val) => val.toLowerCase()),
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Response Types
|
||||
// ============================================================================
|
||||
|
||||
export const UserProfileSchema = z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
username: z.string().nullable(),
|
||||
email: z.string().email(),
|
||||
image: z.string().nullable(),
|
||||
createdAt: z.date(),
|
||||
});
|
||||
|
||||
export const PublicUserSchema = z.object({
|
||||
id: z.string(),
|
||||
username: z.string(),
|
||||
name: z.string(),
|
||||
image: z.string().nullable(),
|
||||
});
|
||||
|
||||
export const UsernameAvailabilitySchema = z.object({
|
||||
username: z.string(),
|
||||
available: z.boolean(),
|
||||
reason: z.string().optional(),
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Type Exports
|
||||
// ============================================================================
|
||||
|
||||
export type UpdateUserProfileInput = z.infer<typeof UpdateUserProfileSchema>;
|
||||
export type CheckUsernameInput = z.infer<typeof CheckUsernameSchema>;
|
||||
export type UserProfile = z.infer<typeof UserProfileSchema>;
|
||||
export type PublicUser = z.infer<typeof PublicUserSchema>;
|
||||
export type UsernameAvailability = z.infer<typeof UsernameAvailabilitySchema>;
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Convert a display name to a URL-friendly username suggestion.
|
||||
*/
|
||||
export function suggestUsername(name: string): string {
|
||||
return name
|
||||
.toLowerCase()
|
||||
.trim()
|
||||
.replace(/[^\w\s-]/g, '') // Remove special chars
|
||||
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
.replace(/-+/g, '-') // Remove consecutive hyphens
|
||||
.replace(/^-+|-+$/g, '') // Trim hyphens
|
||||
.slice(0, 30);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a username is valid (without checking availability).
|
||||
*/
|
||||
export function isValidUsername(username: string): boolean {
|
||||
return UsernameSchema.safeParse(username).success;
|
||||
}
|
||||
|
|
@ -1,7 +1,14 @@
|
|||
import { defineConfig } from 'tsup';
|
||||
|
||||
export default defineConfig({
|
||||
entry: ['src/tool.ts', 'src/registry.ts', 'src/tpmjs.ts', 'src/collection.ts', 'src/agent.ts'],
|
||||
entry: [
|
||||
'src/tool.ts',
|
||||
'src/registry.ts',
|
||||
'src/tpmjs.ts',
|
||||
'src/collection.ts',
|
||||
'src/agent.ts',
|
||||
'src/user.ts',
|
||||
],
|
||||
format: ['esm'],
|
||||
dts: true,
|
||||
clean: true,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue