feat(ui): add Button, Modal, Spinner, Toast, and Tooltip components with styles
All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s

- Implemented Button component with various props for customization.
- Created Modal component with header, content, and footer subcomponents.
- Added Spinner component for loading indicators.
- Developed Toast component for displaying notifications.
- Introduced Tooltip component for contextual hints with keyboard shortcuts.
- Added corresponding CSS modules for styling each component.
- Updated index file to export new components.
- Configured TypeScript settings for the UI package.
This commit is contained in:
Bryan1029384756
2026-04-14 09:02:14 -05:00
parent 9ef839938e
commit b7a4cf4ce8
376 changed files with 52619 additions and 167641 deletions

View File

@@ -12,12 +12,20 @@ export const list = query({
emojis.map(async (emoji) => {
const src = await getPublicStorageUrl(ctx, emoji.storageId);
const user = await ctx.db.get(emoji.uploadedBy);
let avatarUrl: string | null = null;
if (user?.avatarStorageId) {
avatarUrl = await getPublicStorageUrl(ctx, user.avatarStorageId);
}
return {
_id: emoji._id,
name: emoji.name,
src,
createdAt: emoji.createdAt,
animated: emoji.animated ?? false,
uploadedById: emoji.uploadedBy,
uploadedByUsername: user?.username || "Unknown",
uploadedByDisplayName: user?.displayName || null,
uploadedByAvatarUrl: avatarUrl,
};
})
);
@@ -30,6 +38,7 @@ export const upload = mutation({
userId: v.id("userProfiles"),
name: v.string(),
storageId: v.id("_storage"),
animated: v.optional(v.boolean()),
},
returns: v.null(),
handler: async (ctx, args) => {
@@ -64,6 +73,7 @@ export const upload = mutation({
name,
storageId: args.storageId,
uploadedBy: args.userId,
animated: args.animated ?? false,
createdAt: Date.now(),
});
@@ -71,6 +81,53 @@ export const upload = mutation({
},
});
/**
* Rename a custom emoji in place. Enforces the same name validation
* as `upload` and rejects collisions with other existing emojis so
* two rows can't end up sharing a shortcode.
*/
export const rename = mutation({
args: {
userId: v.id("userProfiles"),
emojiId: v.id("customEmojis"),
name: v.string(),
},
returns: v.null(),
handler: async (ctx, args) => {
const roles = await getRolesForUser(ctx, args.userId);
const canManage = roles.some(
(role) => (role.permissions as Record<string, boolean>)?.["manage_channels"]
);
if (!canManage) {
throw new Error("You don't have permission to manage emojis");
}
const emoji = await ctx.db.get(args.emojiId);
if (!emoji) throw new Error("Emoji not found");
const name = args.name.trim();
if (!/^[a-zA-Z0-9_]+$/.test(name)) {
throw new Error("Emoji name can only contain letters, numbers, and underscores");
}
if (name.length < 2 || name.length > 32) {
throw new Error("Emoji name must be between 2 and 32 characters");
}
if (name !== emoji.name) {
const clash = await ctx.db
.query("customEmojis")
.withIndex("by_name", (q) => q.eq("name", name))
.first();
if (clash && clash._id !== args.emojiId) {
throw new Error(`A custom emoji named "${name}" already exists`);
}
}
await ctx.db.patch(args.emojiId, { name });
return null;
},
});
export const remove = mutation({
args: {
userId: v.id("userProfiles"),