Compare commits

...

1 Commits

Author SHA1 Message Date
tonyaellie
12d6b17318 feat: mock ui 2025-06-02 12:08:29 +00:00
18 changed files with 893 additions and 36 deletions

View File

@@ -0,0 +1,130 @@
<template>
<Popover v-model:open="open">
<PopoverTrigger as-child>
<Button
variant="outline"
role="combobox"
:aria-expanded="open"
class="w-full justify-between"
>
{{ value && value.name ? value.name : "Select inventory" }}
<div class="flex items-center gap-2" v-if="value">
<Badge
class="whitespace-nowrap"
:variant="value.role === 'owner' ? 'default' : value.role === 'admin' ? 'secondary' : 'outline'"
>
{{ value.role }}
</Badge>
</div>
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-[--reka-popper-anchor-width] p-0">
<Command :ignore-filter="true">
<CommandInput v-model="search" placeholder="Search collections..." :display-value="(_) => ''" />
<CommandEmpty>No inventory found</CommandEmpty>
<CommandList>
<CommandGroup heading="Your Collections">
<CommandItem
v-for="org in filteredOrgs"
:key="org.id"
:value="org.id"
@select="selectOrg(org as unknown as OrgSummary)"
>
<Check :class="cn('mr-2 h-4 w-4', value?.id === org.id ? 'opacity-100' : 'opacity-0')" />
<div class="flex w-full items-center justify-between gap-2">
{{ org.name }}
<div class="flex items-center gap-2">
<Badge
class="whitespace-nowrap"
variant="outline"
>
{{ org.count }}
</Badge>
<Badge
class="whitespace-nowrap"
:variant="org.role === 'owner' ? 'default' : org.role === 'admin' ? 'secondary' : 'outline'"
>
{{ org.role }}
</Badge>
</div>
</div>
</CommandItem>
</CommandGroup>
<CommandGroup>
<CommandItem @select="() => {}">
<Plus class="mr-2 size-4" /> Create New Collection
</CommandItem>
<CommandItem @select="() => {}">
<Plus class="mr-2 size-4" /> Join Existing Collection
</CommandItem>
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</template>
<script setup lang="ts">
import { Check, ChevronsUpDown, Lock, Users, Plus } from "lucide-vue-next";
import fuzzysort from "fuzzysort";
import { Button } from "~/components/ui/button";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "~/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover";
import { Badge } from "@/components/ui/badge";
import { cn } from "~/lib/utils";
import { ref, computed, watch } from "vue";
import { useVModel } from "@vueuse/core";
type OrgSummary = {
id: string;
name: string;
count: number;
role: "owner" | "admin" | "editor" | "viewer";
type: "personal" | "org";
};
type Props = {
modelValue?: OrgSummary | null;
};
const props = defineProps<Props>();
const emit = defineEmits(["update:modelValue"]);
const open = ref(false);
const search = ref("");
const value = useVModel(props, "modelValue", emit);
// Mock data for demonstration purposes
const orgs = ref<OrgSummary[]>([
{ id: "1", name: "Personal Inventory", count: 1, role: "owner", type: "personal" },
{ id: "2", name: "Family Home", count: 4, role: "admin", type: "org" },
{ id: "3", name: "Office Equipment", count: 12, role: "editor", type: "org" },
{ id: "4", name: "Workshop Tools", count: 3, role: "viewer", type: "org" },
]);
function selectOrg(org: OrgSummary) {
if (value.value?.id !== org.id) {
value.value = org;
} else {
value.value = null;
}
open.value = false;
}
const filteredOrgs = computed(() => {
const filtered = fuzzysort.go(search.value, orgs.value, { key: "name", all: true }).map((i) => i.obj);
return filtered;
});
// Reset search when value is cleared
watch(
() => value.value,
() => {
if (!value.value) {
search.value = "";
}
},
);
</script>

View File

@@ -1,18 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import type { HTMLAttributes } from "vue"; import type { HTMLAttributes } from "vue";
import { Primitive, type PrimitiveProps } from "reka-ui"; import { Primitive, type PrimitiveProps } from "reka-ui";
import { type ButtonVariants, buttonVariants } from "."; import { type ButtonVariants, buttonVariants } from ".";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
interface Props extends PrimitiveProps { interface Props extends PrimitiveProps {
variant?: ButtonVariants["variant"]; variant?: ButtonVariants["variant"];
size?: ButtonVariants["size"]; size?: ButtonVariants["size"];
class?: HTMLAttributes["class"]; class?: HTMLAttributes["class"];
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
as: "button", as: "button",
}); });
</script> </script>
<template> <template>

View File

@@ -0,0 +1,57 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarRoot, type CalendarRootEmits, type CalendarRootProps, useForwardPropsEmits } from 'reka-ui'
import { cn } from '@/lib/utils'
import { CalendarCell, CalendarCellTrigger, CalendarGrid, CalendarGridBody, CalendarGridHead, CalendarGridRow, CalendarHeadCell, CalendarHeader, CalendarHeading, CalendarNextButton, CalendarPrevButton } from '.'
const props = defineProps<CalendarRootProps & { class?: HTMLAttributes['class'] }>()
const emits = defineEmits<CalendarRootEmits>()
const delegatedProps = reactiveOmit(props, 'class')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<CalendarRoot
v-slot="{ grid, weekDays }"
:class="cn('p-3', props.class)"
v-bind="forwarded"
>
<CalendarHeader>
<CalendarPrevButton />
<CalendarHeading />
<CalendarNextButton />
</CalendarHeader>
<div class="flex flex-col gap-y-4 mt-4 sm:flex-row sm:gap-x-4 sm:gap-y-0">
<CalendarGrid v-for="month in grid" :key="month.value.toString()">
<CalendarGridHead>
<CalendarGridRow>
<CalendarHeadCell
v-for="day in weekDays" :key="day"
>
{{ day }}
</CalendarHeadCell>
</CalendarGridRow>
</CalendarGridHead>
<CalendarGridBody>
<CalendarGridRow v-for="(weekDates, index) in month.rows" :key="`weekDate-${index}`" class="mt-2 w-full">
<CalendarCell
v-for="weekDate in weekDates"
:key="weekDate.toString()"
:date="weekDate"
>
<CalendarCellTrigger
:day="weekDate"
:month="month.value"
/>
</CalendarCell>
</CalendarGridRow>
</CalendarGridBody>
</CalendarGrid>
</div>
</CalendarRoot>
</template>

View File

@@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarCell, type CalendarCellProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarCell
:class="cn('relative h-9 w-9 p-0 text-center text-sm focus-within:relative focus-within:z-20 [&:has([data-selected])]:rounded-md [&:has([data-selected])]:bg-accent [&:has([data-selected][data-outside-view])]:bg-accent/50', props.class)"
v-bind="forwardedProps"
>
<slot />
</CalendarCell>
</template>

View File

@@ -0,0 +1,35 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarCellTrigger, type CalendarCellTriggerProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<CalendarCellTriggerProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarCellTrigger
:class="cn(
buttonVariants({ variant: 'ghost' }),
'h-9 w-9 p-0 font-normal',
'[&[data-today]:not([data-selected])]:bg-accent [&[data-today]:not([data-selected])]:text-accent-foreground',
// Selected
'data-[selected]:bg-primary data-[selected]:text-primary-foreground data-[selected]:opacity-100 data-[selected]:hover:bg-primary data-[selected]:hover:text-primary-foreground data-[selected]:focus:bg-primary data-[selected]:focus:text-primary-foreground',
// Disabled
'data-[disabled]:text-muted-foreground data-[disabled]:opacity-50',
// Unavailable
'data-[unavailable]:text-destructive-foreground data-[unavailable]:line-through',
// Outside months
'data-[outside-view]:text-muted-foreground data-[outside-view]:opacity-50 [&[data-outside-view][data-selected]]:bg-accent/50 [&[data-outside-view][data-selected]]:text-muted-foreground [&[data-outside-view][data-selected]]:opacity-30',
props.class,
)"
v-bind="forwardedProps"
>
<slot />
</CalendarCellTrigger>
</template>

View File

@@ -0,0 +1,21 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarGrid, type CalendarGridProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarGridProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarGrid
:class="cn('w-full border-collapse space-y-1', props.class)"
v-bind="forwardedProps"
>
<slot />
</CalendarGrid>
</template>

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { CalendarGridBody, type CalendarGridBodyProps } from 'reka-ui'
const props = defineProps<CalendarGridBodyProps>()
</script>
<template>
<CalendarGridBody v-bind="props">
<slot />
</CalendarGridBody>
</template>

View File

@@ -0,0 +1,11 @@
<script lang="ts" setup>
import { CalendarGridHead, type CalendarGridHeadProps } from 'reka-ui'
const props = defineProps<CalendarGridHeadProps>()
</script>
<template>
<CalendarGridHead v-bind="props">
<slot />
</CalendarGridHead>
</template>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarGridRow, type CalendarGridRowProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarGridRowProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarGridRow :class="cn('flex', props.class)" v-bind="forwardedProps">
<slot />
</CalendarGridRow>
</template>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarHeadCell, type CalendarHeadCellProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeadCellProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeadCell :class="cn('w-9 rounded-md text-[0.8rem] font-normal text-muted-foreground', props.class)" v-bind="forwardedProps">
<slot />
</CalendarHeadCell>
</template>

View File

@@ -0,0 +1,18 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarHeader, type CalendarHeaderProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeaderProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeader :class="cn('relative flex w-full items-center justify-between pt-1', props.class)" v-bind="forwardedProps">
<slot />
</CalendarHeader>
</template>

View File

@@ -0,0 +1,28 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { CalendarHeading, type CalendarHeadingProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
const props = defineProps<CalendarHeadingProps & { class?: HTMLAttributes['class'] }>()
defineSlots<{
default: (props: { headingValue: string }) => any
}>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarHeading
v-slot="{ headingValue }"
:class="cn('text-sm font-medium', props.class)"
v-bind="forwardedProps"
>
<slot :heading-value>
{{ headingValue }}
</slot>
</CalendarHeading>
</template>

View File

@@ -0,0 +1,29 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ChevronRight } from 'lucide-vue-next'
import { CalendarNext, type CalendarNextProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<CalendarNextProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarNext
:class="cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class,
)"
v-bind="forwardedProps"
>
<slot>
<ChevronRight class="h-4 w-4" />
</slot>
</CalendarNext>
</template>

View File

@@ -0,0 +1,29 @@
<script lang="ts" setup>
import type { HTMLAttributes } from 'vue'
import { reactiveOmit } from '@vueuse/core'
import { ChevronLeft } from 'lucide-vue-next'
import { CalendarPrev, type CalendarPrevProps, useForwardProps } from 'reka-ui'
import { cn } from '@/lib/utils'
import { buttonVariants } from '@/components/ui/button'
const props = defineProps<CalendarPrevProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<CalendarPrev
:class="cn(
buttonVariants({ variant: 'outline' }),
'h-7 w-7 bg-transparent p-0 opacity-50 hover:opacity-100',
props.class,
)"
v-bind="forwardedProps"
>
<slot>
<ChevronLeft class="h-4 w-4" />
</slot>
</CalendarPrev>
</template>

View File

@@ -0,0 +1,12 @@
export { default as Calendar } from './Calendar.vue'
export { default as CalendarCell } from './CalendarCell.vue'
export { default as CalendarCellTrigger } from './CalendarCellTrigger.vue'
export { default as CalendarGrid } from './CalendarGrid.vue'
export { default as CalendarGridBody } from './CalendarGridBody.vue'
export { default as CalendarGridHead } from './CalendarGridHead.vue'
export { default as CalendarGridRow } from './CalendarGridRow.vue'
export { default as CalendarHeadCell } from './CalendarHeadCell.vue'
export { default as CalendarHeader } from './CalendarHeader.vue'
export { default as CalendarHeading } from './CalendarHeading.vue'
export { default as CalendarNextButton } from './CalendarNextButton.vue'
export { default as CalendarPrevButton } from './CalendarPrevButton.vue'

View File

@@ -23,6 +23,7 @@
<AppLogo /> <AppLogo />
</div> </div>
</NuxtLink> </NuxtLink>
<!-- <AppOrgSelector v-model:model-value="selectedOrg" /> -->
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger as-child> <DropdownMenuTrigger as-child>
<SidebarMenuButton <SidebarMenuButton
@@ -84,7 +85,20 @@
<span>{{ $t("menu.scanner") }}</span> <span>{{ $t("menu.scanner") }}</span>
</SidebarMenuButton> </SidebarMenuButton>
</SidebarMenuItem> </SidebarMenuItem>
</SidebarMenu>
<SidebarMenuItem>
<SidebarMenuButton
:class="{
'text-nowrap': typeof locale === 'string' && locale.startsWith('zh-'),
}"
:tooltip="$t('menu.scanner')"
@click.prevent="openDialog('scanner')"
>
<MdiAccount />
<span>Collection Settings</span>
</SidebarMenuButton>
</SidebarMenuItem>
</SidebarMenu>
</SidebarGroup> </SidebarGroup>
</SidebarContent> </SidebarContent>
@@ -111,18 +125,29 @@
<SidebarTrigger class="absolute left-2 top-2 hidden lg:flex" variant="default" /> <SidebarTrigger class="absolute left-2 top-2 hidden lg:flex" variant="default" />
</div> </div>
<div <div
class="sticky top-0 z-20 flex h-28 translate-y-[-0.5px] flex-col bg-secondary p-2 shadow-md sm:h-16 sm:flex-row" class="sticky top-0 z-20 flex h-28 translate-y-[-0.5px] flex-col bg-secondary p-2 shadow-md sm:h-16 sm:flex-row justify-between"
:class="{ :class="{
'lg:hidden': preferences.displayLegacyHeader, 'lg:hidden': preferences.displayLegacyHeader,
}" }"
> >
<div class="flex h-1/2 items-center gap-2 sm:h-auto"> <div class="flex h-1/2 items-center gap-2 sm:h-auto">
<div>
<SidebarTrigger variant="default" /> <SidebarTrigger variant="default" />
</div>
<!-- <div>
<Button size="icon">
<AppLogo class="size-8" />
</Button>
</div> -->
<NuxtLink to="/home"> <NuxtLink to="/home">
<AppHeaderText class="h-6" /> <AppHeaderText class="h-6" />
</NuxtLink> </NuxtLink>
<!-- <AppOrgSelector v-model:model-value="selectedOrg" /> -->
</div> </div>
<div class="sm:grow"></div> <!-- <div class="flex items-center">
<AppOrgSelector v-model:model-value="selectedOrg" />
</div> -->
<div class="flex h-1/2 grow items-center justify-end gap-2 sm:h-auto"> <div class="flex h-1/2 grow items-center justify-end gap-2 sm:h-auto">
<Input <Input
v-model:model-value="search" v-model:model-value="search"
@@ -209,6 +234,8 @@
import { Button } from "~/components/ui/button"; import { Button } from "~/components/ui/button";
import { toast } from "@/components/ui/sonner"; import { toast } from "@/components/ui/sonner";
const selectedOrg = ref<any>();
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const username = computed(() => authCtx.user?.name || "User"); const username = computed(() => authCtx.user?.name || "User");
@@ -314,13 +341,13 @@
name: computed(() => t("menu.maintenance")), name: computed(() => t("menu.maintenance")),
to: "/maintenance", to: "/maintenance",
}, },
{ // {
icon: MdiAccount, // icon: MdiAccount,
id: 5, // id: 5,
active: computed(() => route.path === "/profile"), // active: computed(() => route.path === "/profile"),
name: computed(() => t("menu.profile")), // name: computed(() => t("menu.profile")),
to: "/profile", // to: "/profile",
}, // },
{ {
icon: MdiCog, icon: MdiCog,
id: 6, id: 6,
@@ -368,6 +395,36 @@
const authCtx = useAuthContext(); const authCtx = useAuthContext();
const api = useUserApi(); const api = useUserApi();
const checkAuth = async () => {
try {
// await api.user.self();
} catch (err) {
console.log(err);
// if (!authCtx.isAuthorized()) {
// console.log("Not authorised, redirecting to login");
// await navigateTo("/");
// }
}
};
onMounted(() => {
checkAuth();
});
onMounted(() => {
const handleVisibilityChange = () => {
if (!document.hidden) {
checkAuth();
}
};
document.addEventListener("visibilitychange", handleVisibilityChange);
onUnmounted(() => {
document.removeEventListener("visibilitychange", handleVisibilityChange);
});
});
async function logout() { async function logout() {
await authCtx.logout(api); await authCtx.logout(api);
navigateTo("/"); navigateTo("/");

View File

@@ -0,0 +1,362 @@
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { v4 as uuidv4 } from 'uuid'; // For generating unique invite IDs
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Card } from '@/components/ui/card'; // Assuming you have a Card component
import { Badge } from '@/components/ui/badge'; // Assuming you have a Badge component
import {
Calendar as CalendarIcon,
PlusCircle,
Trash,
} from 'lucide-vue-next'; // Icons
import { Calendar } from '@/components/ui/calendar';
import { format } from 'date-fns';
const { t } = useI18n();
definePageMeta({
middleware: ['auth'],
});
useHead({
title: 'HomeBox | ' + t('menu.maintenance'),
});
interface User {
username: string;
id: string;
role: 'owner' | 'admin' | 'editor' | 'viewer';
lastActive: string;
added: string;
}
interface Invite {
id: string;
code: string;
expiresAt: Date | null;
maxUses: number | null;
uses: number;
}
const users = ref<User[]>([
{
username: 'tonya',
id: '1',
role: 'owner',
lastActive: '12 hours ago',
added: '13 hours ago',
},
{
username: 'steve',
id: '2',
role: 'admin',
lastActive: '1 day ago',
added: '2 days ago',
},
{
username: 'bob',
id: '3',
role: 'editor',
lastActive: '30 minutes ago',
added: '5 hours ago',
},
{
username: 'john',
id: '4',
role: 'viewer',
lastActive: '2 hours ago',
added: '1 day ago',
},
]);
const invites = ref<Invite[]>([
{
id: uuidv4(),
code: 'ABCDEF',
expiresAt: null,
maxUses: null,
uses: 0,
},
{
id: uuidv4(),
code: 'GHIJKL',
expiresAt: new Date(new Date().setDate(new Date().getDate() + 7)), // Expires in 7 days
maxUses: 5,
uses: 2,
},
]);
const newInviteExpiresAt = ref<Date | null>(null);
const newInviteMaxUses = ref<number | null>(null);
const page = ref(1);
const roles = ['owner', 'admin', 'editor', 'viewer'];
function handleRoleChange(userId: string, newRole: string) {
const userIndex = users.value.findIndex((user) => user.id === userId);
if (userIndex !== -1) {
users.value[userIndex].role = newRole as
| 'owner'
| 'admin'
| 'editor'
| 'viewer';
}
}
function handleRemoveUser(userId: string) {
users.value = users.value.filter((user) => user.id !== userId);
}
function generateInviteCode() {
return Math.random().toString(36).substring(2, 8).toUpperCase();
}
function createNewInvite() {
const newInvite: Invite = {
id: uuidv4(),
code: generateInviteCode(),
expiresAt: newInviteExpiresAt.value,
maxUses: newInviteMaxUses.value,
uses: 0,
};
invites.value.push(newInvite);
newInviteExpiresAt.value = null;
newInviteMaxUses.value = null;
}
function deleteInvite(inviteId: string) {
invites.value = invites.value.filter((invite) => invite.id !== inviteId);
}
</script>
<template>
<div>
<BaseContainer class="flex flex-col gap-4">
<BaseSectionHeader> Collection Settings </BaseSectionHeader>
<ButtonGroup>
<Button
size="sm"
:variant="page == 1 ? 'default' : 'outline'"
@click="page = 1"
>
Users
</Button>
<Button
size="sm"
:variant="page == 2 ? 'default' : 'outline'"
@click="page = 2"
>
Invites
</Button>
<Button
size="sm"
:variant="page == 3 ? 'default' : 'outline'"
@click="page = 3"
>
Settings
</Button>
</ButtonGroup>
<Card v-if="page == 1" class="p-4 m-4">
<Table>
<TableHeader>
<TableRow>
<TableHead>Username</TableHead>
<TableHead>Role</TableHead>
<TableHead>Last Active</TableHead>
<TableHead>Added</TableHead>
<TableHead class="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="user in users" :key="user.id">
<TableCell class="font-medium">
{{ user.username }}
</TableCell>
<TableCell>
<Badge
:variant="
user.role === 'owner'
? 'default'
: user.role === 'admin'
? 'secondary'
: 'outline'
"
>
{{ user.role }}
</Badge>
</TableCell>
<TableCell>{{ user.lastActive }}</TableCell>
<TableCell>{{ user.added }}</TableCell>
<TableCell class="text-right">
<Popover>
<PopoverTrigger as-child>
<Button size="sm" variant="outline"> Edit </Button>
</PopoverTrigger>
<PopoverContent class="w-48">
<div class="grid gap-4">
<div class="space-y-2">
<h4 class="font-medium leading-none">Edit User</h4>
<p class="text-sm text-muted-foreground">
{{ user.username }}
</p>
</div>
<div class="grid gap-2">
<Label for="role">Role</Label>
<Select
:model-value="user.role"
@update:model-value="
(newRole) => handleRoleChange(user.id, newRole)
"
>
<SelectTrigger>
<SelectValue placeholder="Select a role" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-for="role in roles"
:key="role"
:value="role"
>
{{ role }}
</SelectItem>
</SelectContent>
</Select>
</div>
<Button
variant="destructive"
size="sm"
@click="handleRemoveUser(user.id)"
>
Remove User
</Button>
</div>
</PopoverContent>
</Popover>
</TableCell>
</TableRow>
</TableBody>
</Table>
</Card>
<Card v-if="page == 2" class="p-4 m-4">
<div class="flex flex-col gap-4">
<h3 class="text-lg font-semibold">Existing Invites</h3>
<Table>
<TableHeader>
<TableRow>
<TableHead>Code</TableHead>
<TableHead>Expires</TableHead>
<TableHead>Max Uses</TableHead>
<TableHead>Uses</TableHead>
<TableHead class="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="invite in invites" :key="invite.id">
<TableCell class="font-medium">
{{ invite.code }}
</TableCell>
<TableCell>
{{
invite.expiresAt
? format(invite.expiresAt, 'PPP')
: 'Never'
}}
</TableCell>
<TableCell>
{{ invite.maxUses !== null ? invite.maxUses : 'Unlimited' }}
</TableCell>
<TableCell>{{ invite.uses }}</TableCell>
<TableCell class="text-right">
<Button
variant="destructive"
size="icon"
@click="deleteInvite(invite.id)"
>
<Trash class="w-4 h-4" />
</Button>
</TableCell>
</TableRow>
</TableBody>
</Table>
<hr class="my-4" />
<h3 class="text-lg font-semibold">Create New Invite</h3>
<div class="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
<div class="flex flex-col gap-2">
<Label for="new-invite-max-uses">Max Uses (optional)</Label>
<Input
id="new-invite-max-uses"
type="number"
v-model.number="newInviteMaxUses"
placeholder="Unlimited"
/>
</div>
<div class="flex flex-col gap-2">
<Label for="new-invite-expires-at">Expires At (optional)</Label>
<Popover>
<PopoverTrigger as-child>
<Button
variant="outline"
class="w-full justify-start text-left font-normal"
:class="
!newInviteExpiresAt && 'text-muted-foreground'
"
>
<CalendarIcon class="mr-2 h-4 w-4" />
{{
newInviteExpiresAt
? format(newInviteExpiresAt, 'PPP')
: 'Pick a date'
}}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<Calendar v-model:model-value="newInviteExpiresAt" />
</PopoverContent>
</Popover>
</div>
<div class="flex items-end">
<Button @click="createNewInvite" class="w-full">
<PlusCircle class="mr-2 w-4 h-4" /> Generate Invite
</Button>
</div>
</div>
</div>
</Card>
<Card v-if="page == 3" class="p-4 m-4">
<h3 class="text-lg font-semibold">Collection Settings</h3>
<p class="text-muted-foreground">
This is where you would configure general collection settings.
</p>
<!-- Add your settings forms/components here -->
</Card>
</BaseContainer>
</div>
</template>

View File

@@ -8551,12 +8551,12 @@ snapshots:
'@nuxtjs/eslint-config-typescript@12.1.0(eslint@8.57.1)(typescript@5.6.2)': '@nuxtjs/eslint-config-typescript@12.1.0(eslint@8.57.1)(typescript@5.6.2)':
dependencies: dependencies:
'@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) '@nuxtjs/eslint-config': 12.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
'@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2) '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2)
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.2) '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.2)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-vue: 9.33.0(eslint@8.57.1) eslint-plugin-vue: 9.33.0(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- eslint-import-resolver-webpack - eslint-import-resolver-webpack
@@ -8564,11 +8564,11 @@ snapshots:
- supports-color - supports-color
- typescript - typescript
'@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1)': '@nuxtjs/eslint-config@12.0.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)':
dependencies: dependencies:
eslint: 8.57.1 eslint: 8.57.1
eslint-config-standard: 17.1.0(eslint-plugin-import@2.31.0)(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1) eslint-config-standard: 17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-n: 15.7.0(eslint@8.57.1) eslint-plugin-n: 15.7.0(eslint@8.57.1)
eslint-plugin-node: 11.1.0(eslint@8.57.1) eslint-plugin-node: 11.1.0(eslint@8.57.1)
eslint-plugin-promise: 6.6.0(eslint@8.57.1) eslint-plugin-promise: 6.6.0(eslint@8.57.1)
@@ -10655,10 +10655,10 @@ snapshots:
dependencies: dependencies:
eslint: 8.57.1 eslint: 8.57.1
eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0)(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1): eslint-config-standard@17.1.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1))(eslint-plugin-n@15.7.0(eslint@8.57.1))(eslint-plugin-promise@6.6.0(eslint@8.57.1))(eslint@8.57.1):
dependencies: dependencies:
eslint: 8.57.1 eslint: 8.57.1
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-n: 15.7.0(eslint@8.57.1) eslint-plugin-n: 15.7.0(eslint@8.57.1)
eslint-plugin-promise: 6.6.0(eslint@8.57.1) eslint-plugin-promise: 6.6.0(eslint@8.57.1)
@@ -10670,7 +10670,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1): eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1):
dependencies: dependencies:
'@nolyfill/is-core-module': 1.0.39 '@nolyfill/is-core-module': 1.0.39
debug: 4.4.0 debug: 4.4.0
@@ -10681,18 +10681,18 @@ snapshots:
tinyglobby: 0.2.12 tinyglobby: 0.2.12
unrs-resolver: 1.5.0 unrs-resolver: 1.5.0
optionalDependencies: optionalDependencies:
eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1): eslint-module-utils@2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
dependencies: dependencies:
debug: 3.2.7 debug: 3.2.7
optionalDependencies: optionalDependencies:
'@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.2) '@typescript-eslint/parser': 6.21.0(eslint@8.57.1)(typescript@5.6.2)
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0)(eslint@8.57.1) eslint-import-resolver-typescript: 3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -10708,7 +10708,7 @@ snapshots:
eslint-utils: 2.1.0 eslint-utils: 2.1.0
regexpp: 3.2.0 regexpp: 3.2.0
eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1): eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1):
dependencies: dependencies:
'@rtsao/scc': 1.1.0 '@rtsao/scc': 1.1.0
array-includes: 3.1.8 array-includes: 3.1.8
@@ -10719,7 +10719,7 @@ snapshots:
doctrine: 2.1.0 doctrine: 2.1.0
eslint: 8.57.1 eslint: 8.57.1
eslint-import-resolver-node: 0.3.9 eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0)(eslint@8.57.1) eslint-module-utils: 2.12.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.0(eslint-plugin-import@2.31.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1))(eslint@8.57.1))(eslint@8.57.1)
hasown: 2.0.2 hasown: 2.0.2
is-core-module: 2.16.1 is-core-module: 2.16.1
is-glob: 4.0.3 is-glob: 4.0.3