mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 16:38:20 +01:00
feat: ✨ Add emoji preferences
This commit is contained in:
parent
dca7af4b0e
commit
6934a5758e
34
components/preferences/emojis/category.vue
Normal file
34
components/preferences/emojis/category.vue
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<template>
|
||||||
|
<Collapsible :default-open="true">
|
||||||
|
<div class="grid grid-cols-[1fr,auto] gap-4 items-baseline">
|
||||||
|
<h2 class="text-2xl font-semibold tracking-tight">
|
||||||
|
{{ name }}
|
||||||
|
</h2>
|
||||||
|
<CollapsibleTrigger :as-child="true">
|
||||||
|
<Button size="icon" variant="outline" class="[&_svg]:data-[state=open]:-rotate-180">
|
||||||
|
<ChevronDown class="duration-200" />
|
||||||
|
</Button>
|
||||||
|
</CollapsibleTrigger>
|
||||||
|
</div>
|
||||||
|
<CollapsibleContent class="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 mt-4">
|
||||||
|
<Emoji v-for="emoji in emojis" :key="emoji.id" :emoji="emoji" />
|
||||||
|
</CollapsibleContent>
|
||||||
|
</Collapsible>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Emoji as EmojiType } from "@versia/client/types";
|
||||||
|
import { ChevronDown } from "lucide-vue-next";
|
||||||
|
import { Button } from "~/components/ui/button";
|
||||||
|
import {
|
||||||
|
Collapsible,
|
||||||
|
CollapsibleContent,
|
||||||
|
CollapsibleTrigger,
|
||||||
|
} from "~/components/ui/collapsible";
|
||||||
|
import Emoji from "./emoji.vue";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
emojis: EmojiType[];
|
||||||
|
name: string;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
25
components/preferences/emojis/emoji.vue
Normal file
25
components/preferences/emojis/emoji.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<Card class="grid grid-cols-[auto,1fr] gap-4 items-center p-4">
|
||||||
|
<Avatar shape="square">
|
||||||
|
<AvatarImage :src="emoji.url" />
|
||||||
|
</Avatar>
|
||||||
|
<CardHeader class="p-0 gap-0 overflow-hidden">
|
||||||
|
<CardTitle as="span" class="text-sm font-mono truncate">
|
||||||
|
{{ emoji.shortcode }}
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
{{ emoji.global ? "Global" : "Uploaded by you" }}
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Emoji } from "@versia/client/types";
|
||||||
|
import { Avatar } from "~/components/ui/avatar";
|
||||||
|
import { Card, CardDescription, CardTitle } from "~/components/ui/card";
|
||||||
|
|
||||||
|
defineProps<{
|
||||||
|
emoji: Emoji;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="flex flex-col gap-4">
|
|
||||||
<div class="grid grid-cols-[1fr,auto,auto] gap-4 items-baseline">
|
|
||||||
<h2 class="text-xl font-bold">{{ name }}</h2>
|
|
||||||
<!-- <Button theme="primary">
|
|
||||||
<Icon icon="tabler:upload" />
|
|
||||||
<span class="hidden md:block">New</span>
|
|
||||||
</Button> -->
|
|
||||||
<Button theme="outline">
|
|
||||||
<Icon icon="tabler:chevron-up" class="duration-100" :style="{
|
|
||||||
transform: collapsed ? 'rotate(180deg)' : 'rotate(0deg)',
|
|
||||||
}" @click="collapsed = !collapsed" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div ref="container" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 overflow-hidden duration-200">
|
|
||||||
<GridItem v-for="emoji in emojis" :key="emoji.id" :emoji="emoji" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { Emoji } from "@versia/client/types";
|
|
||||||
import Button from "~/packages/ui/components/buttons/button.vue";
|
|
||||||
import Icon from "~/packages/ui/components/icons/icon.vue";
|
|
||||||
import GridItem from "./grid-item.vue";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
emojis: Emoji[];
|
|
||||||
name: string;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const collapsed = ref(false);
|
|
||||||
const container = ref<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
watch(collapsed, (value) => {
|
|
||||||
// Use requestAnimationFrame to prevent layout thrashing
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
if (!container.value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
container.value.style.maxHeight = value
|
|
||||||
? "0px"
|
|
||||||
: `${container.value.scrollHeight}px`;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="max-w-7xl mx-auto py-12 px-4">
|
|
||||||
<div class="md:max-w-sm w-full relative mb-4">
|
|
||||||
<TextInput v-model="search" placeholder="Search" class="pl-8" />
|
|
||||||
<iconify-icon icon="tabler:search"
|
|
||||||
class="absolute size-4 top-1/2 left-2.5 transform -translate-y-1/2 text-gray-200" aria-hidden="true"
|
|
||||||
width="unset" />
|
|
||||||
</div>
|
|
||||||
<Category v-if="emojis.length > 0" v-for="([name, emojis]) in categories" :key="name" :emojis="emojis"
|
|
||||||
:name="name" />
|
|
||||||
<div v-else class="flex flex-col items-center justify-center gap-2 text-gray-200 text-center p-10">
|
|
||||||
<span class="text-lg font-semibold">No emojis found.</span>
|
|
||||||
<span class="text-sm">
|
|
||||||
You can ask your administrator to add some emojis.
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { Emoji } from "@versia/client/types";
|
|
||||||
import TextInput from "~/components/inputs/text-input.vue";
|
|
||||||
import Category from "./category.vue";
|
|
||||||
|
|
||||||
const emojis = computed(() =>
|
|
||||||
((identity.value?.emojis as Emoji[] | undefined) ?? []).filter((emoji) =>
|
|
||||||
emoji.shortcode.toLowerCase().includes(search.value.toLowerCase()),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const search = ref("");
|
|
||||||
|
|
||||||
const categories = computed(() => {
|
|
||||||
const categories = new Map<string, Emoji[]>();
|
|
||||||
for (const emoji of emojis.value) {
|
|
||||||
if (!emoji.category) {
|
|
||||||
if (!categories.has("Uncategorized")) {
|
|
||||||
categories.set("Uncategorized", []);
|
|
||||||
}
|
|
||||||
|
|
||||||
categories.get("Uncategorized")?.push(emoji);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!categories.has(emoji.category)) {
|
|
||||||
categories.set(emoji.category, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
categories.get(emoji.category)?.push(emoji);
|
|
||||||
}
|
|
||||||
return categories;
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
<template>
|
|
||||||
<AdaptiveDropdown>
|
|
||||||
<template #button>
|
|
||||||
<Button theme="outline">
|
|
||||||
<iconify-icon width="none" icon="tabler:dots" class="size-5 text-gray-200"
|
|
||||||
aria-hidden="true" />
|
|
||||||
<span class="sr-only">Open menu</span>
|
|
||||||
</Button>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<template #items>
|
|
||||||
<Menu.ItemGroup>
|
|
||||||
<Menu.Item value="">
|
|
||||||
<ButtonDropdown icon="tabler:trash" class="w-full">
|
|
||||||
Delete
|
|
||||||
</ButtonDropdown>
|
|
||||||
</Menu.Item>
|
|
||||||
</Menu.ItemGroup>
|
|
||||||
</template>
|
|
||||||
</AdaptiveDropdown>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { Menu } from "@ark-ui/vue";
|
|
||||||
import type { Emoji } from "@versia/client/types";
|
|
||||||
import ButtonDropdown from "~/components/buttons/button-dropdown.vue";
|
|
||||||
import AdaptiveDropdown from "~/components/dropdowns/AdaptiveDropdown.vue";
|
|
||||||
import Button from "~/packages/ui/components/buttons/button.vue";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
emoji: Emoji;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
@ -1,17 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="rounded ring-1 m-1 ring-white/10 grid grid-cols-[auto,1fr] gap-x-4 p-3 bg-dark-400 hover:ring-2 hover:ring-primary-600 duration-100 items-center">
|
|
||||||
<Avatar :src="emoji.url" class="size-12 rounded bg-transparent" />
|
|
||||||
<div class="text-ellipsis font-mono text-wrap w-full overflow-hidden">{{ emoji.shortcode }}</div>
|
|
||||||
<!-- <GridItemMenu :emoji="emoji" /> -->
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import type { Emoji } from "@versia/client/types";
|
|
||||||
import Avatar from "~/components/avatars/avatar.vue";
|
|
||||||
import GridItemMenu from "./grid-item-menu.vue";
|
|
||||||
|
|
||||||
defineProps<{
|
|
||||||
emoji: Emoji;
|
|
||||||
}>();
|
|
||||||
</script>
|
|
||||||
|
|
@ -2,10 +2,6 @@ import type { Client } from "@versia/client";
|
||||||
import type { RolePermission } from "@versia/client/types";
|
import type { RolePermission } from "@versia/client/types";
|
||||||
|
|
||||||
export const useCacheRefresh = (client: MaybeRef<Client | null>) => {
|
export const useCacheRefresh = (client: MaybeRef<Client | null>) => {
|
||||||
if (import.meta.server) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Refresh custom emojis and instance data and me on every reload
|
// Refresh custom emojis and instance data and me on every reload
|
||||||
watch(
|
watch(
|
||||||
[identity, client],
|
[identity, client],
|
||||||
|
|
@ -67,6 +63,6 @@ export const useCacheRefresh = (client: MaybeRef<Client | null>) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{ flush: "sync" },
|
{ flush: "sync", immediate: true },
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
65
pages/preferences/emojis.vue
Normal file
65
pages/preferences/emojis.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<div class="md:px-8 px-4 py-2 max-w-7xl mx-auto w-full space-y-6">
|
||||||
|
<h1 class="scroll-m-20 text-3xl font-extrabold tracking-tight lg:text-4xl capitalize">
|
||||||
|
Emojis
|
||||||
|
</h1>
|
||||||
|
<div v-if="emojis.length > 0" class="max-w-sm w-full relative">
|
||||||
|
<Input v-model="search" placeholder="Search" class="pl-8" />
|
||||||
|
<Search class="absolute size-4 top-1/2 left-2.5 transform -translate-y-1/2" />
|
||||||
|
</div>
|
||||||
|
<Category v-if="emojis.length > 0" v-for="([name, emojis]) in categories" :key="name" :emojis="emojis"
|
||||||
|
:name="name" />
|
||||||
|
<Card v-else class="shadow-none bg-transparent border-none p-4">
|
||||||
|
<CardHeader class="text-center gap-y-4">
|
||||||
|
<CardTitle class="text-">No emojis found.</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Ask your administrator to add some emojis.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Emoji } from "@versia/client/types";
|
||||||
|
import { Search } from "lucide-vue-next";
|
||||||
|
import Category from "~/components/preferences/emojis/category.vue";
|
||||||
|
import { Input } from "~/components/ui/input";
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: "app",
|
||||||
|
});
|
||||||
|
|
||||||
|
const emojis = computed(
|
||||||
|
() =>
|
||||||
|
identity.value?.emojis?.filter((emoji) =>
|
||||||
|
emoji.shortcode.toLowerCase().includes(search.value.toLowerCase()),
|
||||||
|
) ?? [],
|
||||||
|
);
|
||||||
|
|
||||||
|
const search = ref("");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort emojis by category
|
||||||
|
*/
|
||||||
|
const categories = computed(() => {
|
||||||
|
const categories = new Map<string, Emoji[]>();
|
||||||
|
for (const emoji of emojis.value) {
|
||||||
|
if (!emoji.category) {
|
||||||
|
if (!categories.has("Uncategorized")) {
|
||||||
|
categories.set("Uncategorized", []);
|
||||||
|
}
|
||||||
|
|
||||||
|
categories.get("Uncategorized")?.push(emoji);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!categories.has(emoji.category)) {
|
||||||
|
categories.set(emoji.category, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
categories.get(emoji.category)?.push(emoji);
|
||||||
|
}
|
||||||
|
return categories;
|
||||||
|
});
|
||||||
|
</script>
|
||||||
Loading…
Reference in a new issue