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
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:
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user