Added base project
@@ -1,7 +1,57 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<router-view />
|
||||
<Toaster richColors />
|
||||
<nav class="max-w-full p-5 flex flex-row justify-between align-middle">
|
||||
<div class="align-middle text-xl">
|
||||
<RouterLink to="/"> {{ pageTitle }} </RouterLink>
|
||||
<span class="text-xs" v-if="authStore.currentUser">
|
||||
({{ authStore.currentUser?.name }})
|
||||
</span>
|
||||
</div>
|
||||
<NavBar @logout="logout" :userLoggedIn="authStore.isLoggedIn" />
|
||||
</nav>
|
||||
<div>
|
||||
<main class="container m-auto">
|
||||
<RouterView />
|
||||
</main>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<script setup>
|
||||
import { RouterLink, RouterView } from 'vue-router';
|
||||
import { toast } from 'vue-sonner';
|
||||
import 'vue-sonner/style.css'
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
import NavBar from './components/layout/NavBar.vue';
|
||||
import { useAuthStore } from './stores/auth';
|
||||
import { useSocketStore } from './stores/socket';
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const socketStore = useSocketStore()
|
||||
|
||||
|
||||
const year = new Date().getFullYear()
|
||||
const pageTitle = ref(`DAD ${year}/${String(year + 1).slice(-2)}`)
|
||||
|
||||
|
||||
|
||||
const logout = () => {
|
||||
|
||||
toast.promise(authStore.logout(), {
|
||||
loading: 'Calling API',
|
||||
success: () => {
|
||||
return 'Logout Sucessfull '
|
||||
},
|
||||
error: (data) => `[API] Error saving game - ${data?.response?.data?.message}`,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
socketStore.handleConnection()
|
||||
})
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<style></style>`
|
||||
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 255 KiB |
|
Before Width: | Height: | Size: 238 KiB |
|
Before Width: | Height: | Size: 273 KiB |
|
Before Width: | Height: | Size: 20 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 242 KiB |
|
Before Width: | Height: | Size: 201 KiB |
|
Before Width: | Height: | Size: 197 KiB |
|
Before Width: | Height: | Size: 21 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 26 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 36 KiB |
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div>
|
||||
<NavigationMenu>
|
||||
<NavigationMenuList class="justify-around gap-20">
|
||||
<NavigationMenuItem>
|
||||
<NavigationMenuTrigger>Testing</NavigationMenuTrigger>
|
||||
<NavigationMenuContent>
|
||||
<li>
|
||||
<NavigationMenuLink as-child>
|
||||
<RouterLink to="/testing/laravel">Laravel</RouterLink>
|
||||
</NavigationMenuLink>
|
||||
<NavigationMenuLink as-child>
|
||||
<RouterLink to="/testing/websockets">Web Sockets</RouterLink>
|
||||
</NavigationMenuLink>
|
||||
</li>
|
||||
</NavigationMenuContent>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem v-if="!userLoggedIn">
|
||||
<NavigationMenuLink>
|
||||
<RouterLink to="/login">Login</RouterLink>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
<NavigationMenuItem v-else>
|
||||
<NavigationMenuLink>
|
||||
<a @click.prevent="logoutClickHandler">Logout</a>
|
||||
</NavigationMenuLink>
|
||||
</NavigationMenuItem>
|
||||
</NavigationMenuList>
|
||||
</NavigationMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
NavigationMenu,
|
||||
NavigationMenuContent,
|
||||
NavigationMenuItem,
|
||||
NavigationMenuLink,
|
||||
NavigationMenuList,
|
||||
NavigationMenuTrigger,
|
||||
} from '@/components/ui/navigation-menu'
|
||||
|
||||
|
||||
const emits = defineEmits(['logout'])
|
||||
const { userLoggedIn } = defineProps(['userLoggedIn'])
|
||||
|
||||
const logoutClickHandler = () => {
|
||||
emits('logout')
|
||||
}
|
||||
</script>
|
||||
|
Before Width: | Height: | Size: 36 KiB |
|
Before Width: | Height: | Size: 210 KiB |
|
Before Width: | Height: | Size: 191 KiB |
|
Before Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 203 KiB |
|
Before Width: | Height: | Size: 279 KiB |
|
Before Width: | Height: | Size: 257 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 34 KiB |
|
Before Width: | Height: | Size: 39 KiB |
|
Before Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 312 KiB |
@@ -0,0 +1,25 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { Primitive } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { badgeVariants } from ".";
|
||||
|
||||
const props = defineProps({
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
variant: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="badge"
|
||||
:class="cn(badgeVariants({ variant }), props.class)"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
export { default as Badge } from "./Badge.vue";
|
||||
|
||||
export const badgeVariants = cva(
|
||||
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { Primitive } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { buttonVariants } from ".";
|
||||
|
||||
const props = defineProps({
|
||||
variant: { type: null, required: false },
|
||||
size: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false, default: "button" },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Primitive
|
||||
data-slot="button"
|
||||
:as="as"
|
||||
:as-child="asChild"
|
||||
:class="cn(buttonVariants({ variant, size }), props.class)"
|
||||
>
|
||||
<slot />
|
||||
</Primitive>
|
||||
</template>
|
||||
@@ -0,0 +1,35 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
export { default as Button } from "./Button.vue";
|
||||
|
||||
export const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||
destructive:
|
||||
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
|
||||
outline:
|
||||
"border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
||||
icon: "size-9",
|
||||
"icon-sm": "size-8",
|
||||
"icon-lg": "size-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
import { useVModel } from "@vueuse/core";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
defaultValue: { type: [String, Number], required: false },
|
||||
modelValue: { type: [String, Number], required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
|
||||
const modelValue = useVModel(props, "modelValue", emits, {
|
||||
passive: true,
|
||||
defaultValue: props.defaultValue,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<input
|
||||
v-model="modelValue"
|
||||
data-slot="input"
|
||||
:class="
|
||||
cn(
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Input } from "./Input.vue";
|
||||
@@ -0,0 +1,45 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { NavigationMenuRoot, useForwardPropsEmits } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
import NavigationMenuViewport from "./NavigationMenuViewport.vue";
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: { type: String, required: false },
|
||||
defaultValue: { type: String, required: false },
|
||||
dir: { type: String, required: false },
|
||||
orientation: { type: String, required: false },
|
||||
delayDuration: { type: Number, required: false },
|
||||
skipDelayDuration: { type: Number, required: false },
|
||||
disableClickTrigger: { type: Boolean, required: false },
|
||||
disableHoverTrigger: { type: Boolean, required: false },
|
||||
disablePointerLeaveClose: { type: Boolean, required: false },
|
||||
unmountOnHide: { type: Boolean, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
viewport: { type: Boolean, required: false, default: true },
|
||||
});
|
||||
const emits = defineEmits(["update:modelValue"]);
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "viewport");
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavigationMenuRoot
|
||||
v-slot="slotProps"
|
||||
data-slot="navigation-menu"
|
||||
:data-viewport="viewport"
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot v-bind="slotProps" />
|
||||
<NavigationMenuViewport v-if="viewport" />
|
||||
</NavigationMenuRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,39 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { NavigationMenuContent, useForwardPropsEmits } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
forceMount: { type: Boolean, required: false },
|
||||
disableOutsidePointerEvents: { type: Boolean, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
const emits = defineEmits([
|
||||
"escapeKeyDown",
|
||||
"pointerDownOutside",
|
||||
"focusOutside",
|
||||
"interactOutside",
|
||||
]);
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavigationMenuContent
|
||||
data-slot="navigation-menu-content"
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto',
|
||||
'group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</NavigationMenuContent>
|
||||
</template>
|
||||
@@ -0,0 +1,33 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { NavigationMenuIndicator, useForwardProps } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
forceMount: { type: Boolean, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavigationMenuIndicator
|
||||
data-slot="navigation-menu-indicator"
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<div
|
||||
class="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md"
|
||||
/>
|
||||
</NavigationMenuIndicator>
|
||||
</template>
|
||||
@@ -0,0 +1,24 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { NavigationMenuItem } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
value: { type: String, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavigationMenuItem
|
||||
data-slot="navigation-menu-item"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('relative', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</NavigationMenuItem>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { NavigationMenuLink, useForwardPropsEmits } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
active: { type: Boolean, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
const emits = defineEmits(["select"]);
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavigationMenuLink
|
||||
data-slot="navigation-menu-link"
|
||||
v-bind="forwarded"
|
||||
:class="
|
||||
cn(
|
||||
'data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground ring-ring/10 dark:ring-ring/20 dark:outline-ring/40 outline-ring/50 [&_svg:not([class*=\'text-\'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-[color,box-shadow] focus-visible:ring-4 focus-visible:outline-1 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</NavigationMenuLink>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { NavigationMenuList, useForwardProps } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavigationMenuList
|
||||
data-slot="navigation-menu-list"
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'group flex flex-1 list-none items-center justify-center gap-1',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</NavigationMenuList>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { ChevronDown } from "lucide-vue-next";
|
||||
import { NavigationMenuTrigger, useForwardProps } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { navigationMenuTriggerStyle } from ".";
|
||||
|
||||
const props = defineProps({
|
||||
disabled: { type: Boolean, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NavigationMenuTrigger
|
||||
data-slot="navigation-menu-trigger"
|
||||
v-bind="forwardedProps"
|
||||
:class="cn(navigationMenuTriggerStyle(), 'group', props.class)"
|
||||
>
|
||||
<slot />
|
||||
<ChevronDown
|
||||
class="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</NavigationMenuTrigger>
|
||||
</template>
|
||||
@@ -0,0 +1,32 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { NavigationMenuViewport, useForwardProps } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
forceMount: { type: Boolean, required: false },
|
||||
align: { type: String, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="absolute top-full left-0 isolate z-50 flex justify-center">
|
||||
<NavigationMenuViewport
|
||||
data-slot="navigation-menu-viewport"
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--reka-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--reka-navigation-menu-viewport-width)] left-[var(--reka-navigation-menu-viewport-left)]',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
import { cva } from "class-variance-authority";
|
||||
|
||||
export { default as NavigationMenu } from "./NavigationMenu.vue";
|
||||
export { default as NavigationMenuContent } from "./NavigationMenuContent.vue";
|
||||
export { default as NavigationMenuIndicator } from "./NavigationMenuIndicator.vue";
|
||||
export { default as NavigationMenuItem } from "./NavigationMenuItem.vue";
|
||||
export { default as NavigationMenuLink } from "./NavigationMenuLink.vue";
|
||||
export { default as NavigationMenuList } from "./NavigationMenuList.vue";
|
||||
export { default as NavigationMenuTrigger } from "./NavigationMenuTrigger.vue";
|
||||
export { default as NavigationMenuViewport } from "./NavigationMenuViewport.vue";
|
||||
|
||||
export const navigationMenuTriggerStyle = cva(
|
||||
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1",
|
||||
);
|
||||
@@ -0,0 +1,26 @@
|
||||
<script setup>
|
||||
import { SelectRoot, useForwardPropsEmits } from "reka-ui";
|
||||
|
||||
const props = defineProps({
|
||||
open: { type: Boolean, required: false },
|
||||
defaultOpen: { type: Boolean, required: false },
|
||||
defaultValue: { type: null, required: false },
|
||||
modelValue: { type: null, required: false },
|
||||
by: { type: [String, Function], required: false },
|
||||
dir: { type: String, required: false },
|
||||
multiple: { type: Boolean, required: false },
|
||||
autocomplete: { type: String, required: false },
|
||||
disabled: { type: Boolean, required: false },
|
||||
name: { type: String, required: false },
|
||||
required: { type: Boolean, required: false },
|
||||
});
|
||||
const emits = defineEmits(["update:modelValue", "update:open"]);
|
||||
|
||||
const forwarded = useForwardPropsEmits(props, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectRoot v-slot="slotProps" data-slot="select" v-bind="forwarded">
|
||||
<slot v-bind="slotProps" />
|
||||
</SelectRoot>
|
||||
</template>
|
||||
@@ -0,0 +1,81 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import {
|
||||
SelectContent,
|
||||
SelectPortal,
|
||||
SelectViewport,
|
||||
useForwardPropsEmits,
|
||||
} from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SelectScrollDownButton, SelectScrollUpButton } from ".";
|
||||
|
||||
defineOptions({
|
||||
inheritAttrs: false,
|
||||
});
|
||||
|
||||
const props = defineProps({
|
||||
forceMount: { type: Boolean, required: false },
|
||||
position: { type: String, required: false, default: "popper" },
|
||||
bodyLock: { type: Boolean, required: false },
|
||||
side: { type: null, required: false },
|
||||
sideOffset: { type: Number, required: false },
|
||||
sideFlip: { type: Boolean, required: false },
|
||||
align: { type: null, required: false },
|
||||
alignOffset: { type: Number, required: false },
|
||||
alignFlip: { type: Boolean, required: false },
|
||||
avoidCollisions: { type: Boolean, required: false },
|
||||
collisionBoundary: { type: null, required: false },
|
||||
collisionPadding: { type: [Number, Object], required: false },
|
||||
arrowPadding: { type: Number, required: false },
|
||||
sticky: { type: String, required: false },
|
||||
hideWhenDetached: { type: Boolean, required: false },
|
||||
positionStrategy: { type: String, required: false },
|
||||
updatePositionStrategy: { type: String, required: false },
|
||||
disableUpdateOnLayoutShift: { type: Boolean, required: false },
|
||||
prioritizePosition: { type: Boolean, required: false },
|
||||
reference: { type: null, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
const emits = defineEmits([
|
||||
"closeAutoFocus",
|
||||
"escapeKeyDown",
|
||||
"pointerDownOutside",
|
||||
]);
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
|
||||
const forwarded = useForwardPropsEmits(delegatedProps, emits);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectPortal>
|
||||
<SelectContent
|
||||
data-slot="select-content"
|
||||
v-bind="{ ...$attrs, ...forwarded }"
|
||||
:class="
|
||||
cn(
|
||||
'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--reka-select-content-available-height) min-w-[8rem] overflow-x-hidden overflow-y-auto rounded-md border shadow-md',
|
||||
position === 'popper' &&
|
||||
'data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<SelectScrollUpButton />
|
||||
<SelectViewport
|
||||
:class="
|
||||
cn(
|
||||
'p-1',
|
||||
position === 'popper' &&
|
||||
'h-[var(--reka-select-trigger-height)] w-full min-w-[var(--reka-select-trigger-width)] scroll-my-1',
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</SelectViewport>
|
||||
<SelectScrollDownButton />
|
||||
</SelectContent>
|
||||
</SelectPortal>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup>
|
||||
import { SelectGroup } from "reka-ui";
|
||||
|
||||
const props = defineProps({
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectGroup data-slot="select-group" v-bind="props">
|
||||
<slot />
|
||||
</SelectGroup>
|
||||
</template>
|
||||
@@ -0,0 +1,49 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { Check } from "lucide-vue-next";
|
||||
import {
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
useForwardProps,
|
||||
} from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
value: { type: null, required: true },
|
||||
disabled: { type: Boolean, required: false },
|
||||
textValue: { type: String, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectItem
|
||||
data-slot="select-item"
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'focus:bg-accent focus:text-accent-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="absolute right-2 flex size-3.5 items-center justify-center">
|
||||
<SelectItemIndicator>
|
||||
<slot name="indicator-icon">
|
||||
<Check class="size-4" />
|
||||
</slot>
|
||||
</SelectItemIndicator>
|
||||
</span>
|
||||
|
||||
<SelectItemText>
|
||||
<slot />
|
||||
</SelectItemText>
|
||||
</SelectItem>
|
||||
</template>
|
||||
@@ -0,0 +1,14 @@
|
||||
<script setup>
|
||||
import { SelectItemText } from "reka-ui";
|
||||
|
||||
const props = defineProps({
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectItemText data-slot="select-item-text" v-bind="props">
|
||||
<slot />
|
||||
</SelectItemText>
|
||||
</template>
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup>
|
||||
import { SelectLabel } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
for: { type: String, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectLabel
|
||||
data-slot="select-label"
|
||||
:class="cn('text-muted-foreground px-2 py-1.5 text-xs', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</SelectLabel>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { ChevronDown } from "lucide-vue-next";
|
||||
import { SelectScrollDownButton, useForwardProps } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectScrollDownButton
|
||||
data-slot="select-scroll-down-button"
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn('flex cursor-default items-center justify-center py-1', props.class)
|
||||
"
|
||||
>
|
||||
<slot>
|
||||
<ChevronDown class="size-4" />
|
||||
</slot>
|
||||
</SelectScrollDownButton>
|
||||
</template>
|
||||
@@ -0,0 +1,30 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { ChevronUp } from "lucide-vue-next";
|
||||
import { SelectScrollUpButton, useForwardProps } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectScrollUpButton
|
||||
data-slot="select-scroll-up-button"
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn('flex cursor-default items-center justify-center py-1', props.class)
|
||||
"
|
||||
>
|
||||
<slot>
|
||||
<ChevronUp class="size-4" />
|
||||
</slot>
|
||||
</SelectScrollUpButton>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { SelectSeparator } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectSeparator
|
||||
data-slot="select-separator"
|
||||
v-bind="delegatedProps"
|
||||
:class="cn('bg-border pointer-events-none -mx-1 my-1 h-px', props.class)"
|
||||
/>
|
||||
</template>
|
||||
@@ -0,0 +1,37 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { ChevronDown } from "lucide-vue-next";
|
||||
import { SelectIcon, SelectTrigger, useForwardProps } from "reka-ui";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
disabled: { type: Boolean, required: false },
|
||||
reference: { type: null, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
class: { type: null, required: false },
|
||||
size: { type: String, required: false, default: "default" },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class", "size");
|
||||
const forwardedProps = useForwardProps(delegatedProps);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectTrigger
|
||||
data-slot="select-trigger"
|
||||
:data-size="size"
|
||||
v-bind="forwardedProps"
|
||||
:class="
|
||||
cn(
|
||||
'border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*=\'text-\'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*=\'size-\'])]:size-4',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
<SelectIcon as-child>
|
||||
<ChevronDown class="size-4 opacity-50" />
|
||||
</SelectIcon>
|
||||
</SelectTrigger>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup>
|
||||
import { SelectValue } from "reka-ui";
|
||||
|
||||
const props = defineProps({
|
||||
placeholder: { type: String, required: false },
|
||||
asChild: { type: Boolean, required: false },
|
||||
as: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SelectValue data-slot="select-value" v-bind="props">
|
||||
<slot />
|
||||
</SelectValue>
|
||||
</template>
|
||||
@@ -0,0 +1,11 @@
|
||||
export { default as Select } from "./Select.vue";
|
||||
export { default as SelectContent } from "./SelectContent.vue";
|
||||
export { default as SelectGroup } from "./SelectGroup.vue";
|
||||
export { default as SelectItem } from "./SelectItem.vue";
|
||||
export { default as SelectItemText } from "./SelectItemText.vue";
|
||||
export { default as SelectLabel } from "./SelectLabel.vue";
|
||||
export { default as SelectScrollDownButton } from "./SelectScrollDownButton.vue";
|
||||
export { default as SelectScrollUpButton } from "./SelectScrollUpButton.vue";
|
||||
export { default as SelectSeparator } from "./SelectSeparator.vue";
|
||||
export { default as SelectTrigger } from "./SelectTrigger.vue";
|
||||
export { default as SelectValue } from "./SelectValue.vue";
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup>
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
InfoIcon,
|
||||
Loader2Icon,
|
||||
OctagonXIcon,
|
||||
TriangleAlertIcon,
|
||||
XIcon,
|
||||
} from "lucide-vue-next";
|
||||
import { Toaster as Sonner } from "vue-sonner";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: String, required: false },
|
||||
invert: { type: Boolean, required: false },
|
||||
theme: { type: String, required: false },
|
||||
position: { type: String, required: false },
|
||||
closeButtonPosition: { type: String, required: false },
|
||||
hotkey: { type: Array, required: false },
|
||||
richColors: { type: Boolean, required: false },
|
||||
expand: { type: Boolean, required: false },
|
||||
duration: { type: Number, required: false },
|
||||
gap: { type: Number, required: false },
|
||||
visibleToasts: { type: Number, required: false },
|
||||
closeButton: { type: Boolean, required: false },
|
||||
toastOptions: { type: Object, required: false },
|
||||
class: { type: String, required: false },
|
||||
style: { type: Object, required: false },
|
||||
offset: { type: [Object, String, Number], required: false },
|
||||
mobileOffset: { type: [Object, String, Number], required: false },
|
||||
dir: { type: String, required: false },
|
||||
swipeDirections: { type: Array, required: false },
|
||||
icons: { type: Object, required: false },
|
||||
containerAriaLabel: { type: String, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Sonner
|
||||
:class="cn('toaster group', props.class)"
|
||||
:style="{
|
||||
'--normal-bg': 'var(--popover)',
|
||||
'--normal-text': 'var(--popover-foreground)',
|
||||
'--normal-border': 'var(--border)',
|
||||
'--border-radius': 'var(--radius)',
|
||||
}"
|
||||
v-bind="props"
|
||||
>
|
||||
<template #success-icon>
|
||||
<CircleCheckIcon class="size-4" />
|
||||
</template>
|
||||
<template #info-icon>
|
||||
<InfoIcon class="size-4" />
|
||||
</template>
|
||||
<template #warning-icon>
|
||||
<TriangleAlertIcon class="size-4" />
|
||||
</template>
|
||||
<template #error-icon>
|
||||
<OctagonXIcon class="size-4" />
|
||||
</template>
|
||||
<template #loading-icon>
|
||||
<div>
|
||||
<Loader2Icon class="size-4 animate-spin" />
|
||||
</div>
|
||||
</template>
|
||||
<template #close-icon>
|
||||
<XIcon class="size-4" />
|
||||
</template>
|
||||
</Sonner>
|
||||
</template>
|
||||
@@ -0,0 +1 @@
|
||||
export { default as Toaster } from "./Sonner.vue";
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div data-slot="table-container" class="relative w-full overflow-auto">
|
||||
<table
|
||||
data-slot="table"
|
||||
:class="cn('w-full caption-bottom text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
:class="cn('[&_tr:last-child]:border-0', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</tbody>
|
||||
</template>
|
||||
@@ -0,0 +1,16 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
:class="cn('text-muted-foreground mt-4 text-sm', props.class)"
|
||||
>
|
||||
<slot />
|
||||
</caption>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
:class="
|
||||
cn(
|
||||
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</td>
|
||||
</template>
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup>
|
||||
import { reactiveOmit } from "@vueuse/core";
|
||||
import { cn } from "@/lib/utils";
|
||||
import TableCell from "./TableCell.vue";
|
||||
import TableRow from "./TableRow.vue";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
colspan: { type: Number, required: false, default: 1 },
|
||||
});
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TableRow>
|
||||
<TableCell
|
||||
:class="
|
||||
cn(
|
||||
'p-4 whitespace-nowrap align-middle text-sm text-foreground',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
v-bind="delegatedProps"
|
||||
>
|
||||
<div class="flex items-center justify-center py-10">
|
||||
<slot />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</template>
|
||||
@@ -0,0 +1,18 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
:class="
|
||||
cn('bg-muted/50 border-t font-medium [&>tr]:last:border-b-0', props.class)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</tfoot>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<th
|
||||
data-slot="table-head"
|
||||
:class="
|
||||
cn(
|
||||
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</th>
|
||||
</template>
|
||||
@@ -0,0 +1,13 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<thead data-slot="table-header" :class="cn('[&_tr]:border-b', props.class)">
|
||||
<slot />
|
||||
</thead>
|
||||
</template>
|
||||
@@ -0,0 +1,21 @@
|
||||
<script setup>
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const props = defineProps({
|
||||
class: { type: null, required: false },
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
:class="
|
||||
cn(
|
||||
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
|
||||
props.class,
|
||||
)
|
||||
"
|
||||
>
|
||||
<slot />
|
||||
</tr>
|
||||
</template>
|
||||
@@ -0,0 +1,9 @@
|
||||
export { default as Table } from "./Table.vue";
|
||||
export { default as TableBody } from "./TableBody.vue";
|
||||
export { default as TableCaption } from "./TableCaption.vue";
|
||||
export { default as TableCell } from "./TableCell.vue";
|
||||
export { default as TableEmpty } from "./TableEmpty.vue";
|
||||
export { default as TableFooter } from "./TableFooter.vue";
|
||||
export { default as TableHead } from "./TableHead.vue";
|
||||
export { default as TableHeader } from "./TableHeader.vue";
|
||||
export { default as TableRow } from "./TableRow.vue";
|
||||
@@ -0,0 +1,7 @@
|
||||
import { isFunction } from "@tanstack/vue-table";
|
||||
|
||||
export function valueUpdater(updaterOrValue, ref) {
|
||||
ref.value = isFunction(updaterOrValue)
|
||||
? updaterOrValue(ref.value)
|
||||
: updaterOrValue;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@plugin "tailwindcss-animate";
|
||||
@import "tw-animate-css";
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
@@ -24,7 +23,6 @@
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
@@ -60,7 +58,6 @@
|
||||
--accent: oklch(0.97 0 0);
|
||||
--accent-foreground: oklch(0.205 0 0);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--destructive-foreground: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.922 0 0);
|
||||
--input: oklch(0.922 0 0);
|
||||
--ring: oklch(0.708 0 0);
|
||||
@@ -82,11 +79,11 @@
|
||||
.dark {
|
||||
--background: oklch(0.145 0 0);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.145 0 0);
|
||||
--card: oklch(0.205 0 0);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.145 0 0);
|
||||
--popover: oklch(0.205 0 0);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.985 0 0);
|
||||
--primary: oklch(0.922 0 0);
|
||||
--primary-foreground: oklch(0.205 0 0);
|
||||
--secondary: oklch(0.269 0 0);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
@@ -94,11 +91,10 @@
|
||||
--muted-foreground: oklch(0.708 0 0);
|
||||
--accent: oklch(0.269 0 0);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.396 0.141 25.723);
|
||||
--destructive-foreground: oklch(0.637 0.237 25.331);
|
||||
--border: oklch(0.269 0 0);
|
||||
--input: oklch(0.269 0 0);
|
||||
--ring: oklch(0.439 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.556 0 0);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
@@ -110,8 +106,8 @@
|
||||
--sidebar-primary-foreground: oklch(0.985 0 0);
|
||||
--sidebar-accent: oklch(0.269 0 0);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(0.269 0 0);
|
||||
--sidebar-ring: oklch(0.439 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.556 0 0);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
@@ -121,4 +117,4 @@
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,22 @@
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
import { io } from 'socket.io-client'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
|
||||
const apiDomain = import.meta.env.VITE_API_DOMAIN
|
||||
const wsConnection = import.meta.env.VITE_WS_CONNECTION
|
||||
|
||||
console.log('[main.js] api domain', apiDomain)
|
||||
console.log('[main.js] ws connection', wsConnection)
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
app.provide('socket', io(wsConnection))
|
||||
app.provide('serverBaseURL', `http://${apiDomain}`)
|
||||
app.provide('apiBaseURL', `http://${apiDomain}/api`)
|
||||
|
||||
app.use(createPinia())
|
||||
app.use(router)
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<h1>Game Board</h1>
|
||||
<p>Game board page content goes here</p>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,8 +0,0 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<h1>Game Selection</h1>
|
||||
<p>Game selection page content goes here</p>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,65 +0,0 @@
|
||||
<script setup>
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="min-h-screen bg-gradient-to-br from-slate-900 via-purple-900 to-slate-900 flex items-center justify-center px-4">
|
||||
<div class="max-w-4xl w-full">
|
||||
<div class="text-center mb-16">
|
||||
<div class="mb-6">
|
||||
<div class="text-7xl mb-4">🎰</div>
|
||||
<h1 class="text-6xl font-bold text-white mb-2">Bisca</h1>
|
||||
<p class="text-xl text-purple-200">The Traditional Portuguese Card Game Online</p>
|
||||
</div>
|
||||
<p class="text-lg text-slate-300 max-w-2xl mx-auto">
|
||||
Challenge players from around the world in this classic card game. Master the rules, build your strategy, and climb the leaderboards.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 mb-12 max-w-2xl mx-auto">
|
||||
<div class="group relative bg-gradient-to-br from-blue-500 to-blue-600 rounded-2xl p-8 hover:shadow-2xl cursor-pointer overflow-hidden"
|
||||
@click="router.push('/game-selection')">
|
||||
<div class="absolute inset-0 bg-white opacity-0 group-hover:opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="text-5xl mb-4">🎮</div>
|
||||
<h2 class="text-2xl font-bold text-white mb-2">Play Game</h2>
|
||||
<p class="text-blue-100 text-sm">Start a new match and test your skills</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="group relative bg-gradient-to-br from-amber-500 to-amber-600 rounded-2xl p-8 hover:shadow-2xl cursor-pointer overflow-hidden"
|
||||
@click="router.push('/leader-boards')">
|
||||
<div class="absolute inset-0 bg-white opacity-0 group-hover:opacity-10"></div>
|
||||
<div class="relative z-10">
|
||||
<div class="text-5xl mb-4">🏆</div>
|
||||
<h2 class="text-2xl font-bold text-white mb-2">Leaderboards</h2>
|
||||
<p class="text-amber-100 text-sm">Compete globally and see the rankings</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 justify-center">
|
||||
<button
|
||||
@click="router.push('/login')"
|
||||
class="px-8 py-3 bg-white text-slate-900 font-bold rounded-lg hover:bg-slate-100 shadow-lg"
|
||||
>
|
||||
Sign In
|
||||
</button>
|
||||
<button
|
||||
@click="router.push('/register')"
|
||||
class="px-8 py-3 border-2 border-white text-white font-bold rounded-lg hover:bg-white hover:text-slate-900"
|
||||
>
|
||||
Create Account
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mt-16 pt-8 border-t border-slate-700 text-center text-slate-400 text-sm">
|
||||
<p>Play anonymously or sign in to compete with others and earn rewards</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,8 +0,0 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<h1>LeaderBoards</h1>
|
||||
<p>LeaderBoards page content goes here</p>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,8 +0,0 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<h1>Profile</h1>
|
||||
<p>Login page content goes here</p>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,8 +0,0 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<h1>Profile</h1>
|
||||
<p>Profile page content goes here</p>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -1,8 +0,0 @@
|
||||
<script setup></script>
|
||||
|
||||
<template>
|
||||
<h1>Profile</h1>
|
||||
<p>Register page content goes here</p>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
@@ -0,0 +1,11 @@
|
||||
<template>
|
||||
<div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped></style>`
|
||||
@@ -0,0 +1,79 @@
|
||||
<template>
|
||||
<div class="flex min-h-screen items-center justify-center bg-gray-50 px-4 py-12 sm:px-6 lg:px-8">
|
||||
<div class="w-full max-w-md space-y-8">
|
||||
<div>
|
||||
<h2 class="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
|
||||
Sign in to your account
|
||||
</h2>
|
||||
<p class="mt-2 text-center text-sm text-gray-600">
|
||||
Enter your credentials to access your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<form class="mt-8 space-y-6" @submit.prevent="handleSubmit">
|
||||
<div class="space-y-4 rounded-md shadow-sm">
|
||||
<div>
|
||||
<label for="email" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Email address
|
||||
</label>
|
||||
<Input id="email" v-model="formData.email" type="email" autocomplete="email" required
|
||||
placeholder="[email protected]" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="password" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Password
|
||||
</label>
|
||||
<Input id="password" v-model="formData.password" type="password" autocomplete="current-password"
|
||||
required placeholder="••••••••" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div>
|
||||
<Button type="submit" class="w-full"> Sign in </Button>
|
||||
</div>
|
||||
|
||||
<div class="text-center text-sm">
|
||||
<span class="text-gray-600">Don't have an account? </span>
|
||||
<a href="#" class="font-medium text-blue-600 hover:text-blue-500">
|
||||
Sign up
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Button } from '@/components/ui/button'
|
||||
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { toast } from 'vue-sonner'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const formData = ref({
|
||||
email: 'pa@mail.pt',
|
||||
password: '123'
|
||||
})
|
||||
|
||||
|
||||
const handleSubmit = async () => {
|
||||
|
||||
toast.promise(authStore.login(formData.value), {
|
||||
loading: 'Calling API',
|
||||
success: (data) => {
|
||||
return `Login Sucessfull - ${data?.name}`
|
||||
},
|
||||
error: (data) => `[API] Error saving game - ${data?.response?.data?.message}`,
|
||||
})
|
||||
|
||||
|
||||
router.push('/')
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,191 @@
|
||||
<template>
|
||||
<div class="max-w-2xl mx-auto py-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">Laravel Tester</h2>
|
||||
|
||||
<div class="space-y-4 mt-10">
|
||||
<div class="flex gap-4">
|
||||
<Select v-model="selectedType" @update:modelValue="handleFiltersChange">
|
||||
<SelectTrigger class="w-[180px]">
|
||||
<SelectValue placeholder="Select type" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="3">Bisca 3</SelectItem>
|
||||
<SelectItem value="9">Bisca 9</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
|
||||
<Select v-model="selectedStatus" @update:modelValue="handleFiltersChange">
|
||||
<SelectTrigger class="w-[180px]">
|
||||
<SelectValue placeholder="Select status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="Pending">Pending</SelectItem>
|
||||
<SelectItem value="Playing">Playing</SelectItem>
|
||||
<SelectItem value="Ended">Ended</SelectItem>
|
||||
<SelectItem value="Interrupted">Interrupted</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ID</TableHead>
|
||||
<TableHead>
|
||||
<div class="flex items-center gap-2 cursor-pointer" @click="toggleSort('type')">
|
||||
Type
|
||||
<ArrowUpDown class="h-4 w-4" />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<div class="flex items-center gap-2 cursor-pointer" @click="toggleSort('status')">
|
||||
Status
|
||||
<ArrowUpDown class="h-4 w-4" />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead>Created By</TableHead>
|
||||
<TableHead>Winner</TableHead>
|
||||
<TableHead>
|
||||
<div class="flex items-center gap-2 cursor-pointer" @click="toggleSort('total_time')">
|
||||
Total Time
|
||||
<ArrowUpDown class="h-4 w-4" />
|
||||
</div>
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<div class="flex items-center gap-2 cursor-pointer" @click="toggleSort('created_at')">
|
||||
Began At
|
||||
<ArrowUpDown class="h-4 w-4" />
|
||||
</div>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow v-for="game in games" :key="game.id">
|
||||
<TableCell>{{ game.id }}</TableCell>
|
||||
<TableCell>
|
||||
<Badge :variant="game.type === '3' ? 'default' : 'secondary'">
|
||||
{{ game.type === '3' ? 'Bisca 3' : 'Bisca 9' }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge :variant="getStatusVariant(game.status)">
|
||||
{{ game.status }}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{{ game.created_by?.name }}</TableCell>
|
||||
<TableCell>{{ game.winner?.name || '-' }}</TableCell>
|
||||
<TableCell>{{ game.total_time ? `${game.total_time}s` : '-' }}</TableCell>
|
||||
<TableCell>{{ formatDate(game.began_at) }}</TableCell>
|
||||
</TableRow>
|
||||
<TableRow v-if="!games?.length">
|
||||
<TableCell colspan="7" class="text-center h-24">No games found</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-center mb-20">
|
||||
<Button variant="outline" @click="loadMore" :disabled="loading">
|
||||
<Loader2 v-if="loading" class="mr-2 h-4 w-4 animate-spin" />
|
||||
Load More
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { format } from 'date-fns'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow
|
||||
} from '@/components/ui/table'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue
|
||||
} from '@/components/ui/select'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { ArrowUpDown, Loader2 } from 'lucide-vue-next'
|
||||
import { useAPIStore } from '@/stores/api';
|
||||
|
||||
|
||||
const apiStore = useAPIStore()
|
||||
const games = ref([])
|
||||
|
||||
const loading = ref(false)
|
||||
const selectedType = ref('')
|
||||
const selectedStatus = ref('')
|
||||
const sortField = ref('began_at')
|
||||
const sortDirection = ref('desc')
|
||||
|
||||
|
||||
const fetchData = async (resetPagination = false) => {
|
||||
loading.value = true
|
||||
|
||||
apiStore.gameQueryParameters.filters = {
|
||||
type: selectedType.value,
|
||||
status: selectedStatus.value,
|
||||
sort_by: sortField.value,
|
||||
sort_direction: sortDirection.value
|
||||
}
|
||||
|
||||
const response = await apiStore.getGames(resetPagination)
|
||||
loading.value = false
|
||||
games.value = response.data.data
|
||||
}
|
||||
|
||||
const handleFiltersChange = async () => {
|
||||
await fetchData(true)
|
||||
}
|
||||
|
||||
const toggleSort = (field) => {
|
||||
if (sortField.value === field) {
|
||||
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'
|
||||
} else {
|
||||
sortField.value = field
|
||||
sortDirection.value = 'desc'
|
||||
}
|
||||
handleFiltersChange()
|
||||
}
|
||||
|
||||
const loadMore = async () => {
|
||||
loading.value = true
|
||||
apiStore.gameQueryParameters.page++
|
||||
const response = await apiStore.getGames()
|
||||
games.value.push(...response.data.data)
|
||||
loading.value = false
|
||||
}
|
||||
|
||||
const getStatusVariant = (status) => {
|
||||
const variants = {
|
||||
Pending: 'secondary',
|
||||
Playing: 'default',
|
||||
Ended: 'success',
|
||||
Interrupted: 'destructive'
|
||||
}
|
||||
return variants[status] || 'default'
|
||||
}
|
||||
|
||||
|
||||
|
||||
const formatDate = (date) => {
|
||||
if (!date) return ''
|
||||
return format(new Date(date), 'PPp')
|
||||
}
|
||||
|
||||
|
||||
onMounted(async () => {
|
||||
await fetchData()
|
||||
})
|
||||
|
||||
</script>
|
||||
@@ -0,0 +1,56 @@
|
||||
<template>
|
||||
<div class="max-w-2xl mx-auto py-8">
|
||||
<h2 class="text-2xl font-bold text-gray-900 mb-6">WebSocket Tester</h2>
|
||||
|
||||
<div class="mb-6 flex items-center space-x-2">
|
||||
<div class="flex items-center">
|
||||
<div class="h-2.5 w-2.5 rounded-full mr-2 animate-pulse"
|
||||
:class="{ 'bg-green-500': socket.connected, 'bg-red-500': !socket.connected, }"></div>
|
||||
<span class="text-sm text-gray-600"> {{ (!socket.connected) ? 'Not' : '' }} Connected</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<form class="space-y-6">
|
||||
<div class="space-y-2">
|
||||
<label for="message" class="block text-sm font-medium text-gray-700">
|
||||
Message:
|
||||
</label>
|
||||
<div class="flex space-x-4">
|
||||
<input type="text" id="message" v-model="message"
|
||||
class="flex-1 px-3 py-2 bg-white border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
|
||||
placeholder="Type your message here...">
|
||||
<Button @click.prevent="send" type="submit">Send </Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="responseData" class="space-y-2 mt-8">
|
||||
<div class="flex justify-between items-center mb-2">
|
||||
<label for="response" class="block text-sm font-medium text-gray-700">
|
||||
Response
|
||||
</label>
|
||||
<span class="text-xs text-gray-500">Real-time updates</span>
|
||||
</div>
|
||||
<textarea :value="responseData" id="response" rows="5"
|
||||
class="mt-1 block w-full px-3 py-2 bg-gray-50 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 sm:text-sm font-mono"
|
||||
readonly></textarea>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
<script setup>
|
||||
import { ref, inject } from 'vue'
|
||||
import Button from '@/components/ui/button/Button.vue';
|
||||
|
||||
const socket = inject('socket')
|
||||
|
||||
const message = ref('DAD Intermediate Submission')
|
||||
const responseData = ref('')
|
||||
|
||||
const send = () => {
|
||||
socket.emit('echo', message.value)
|
||||
}
|
||||
|
||||
socket.on('echo', (message) => {
|
||||
responseData.value = message
|
||||
})
|
||||
</script>
|
||||
@@ -1,50 +1,33 @@
|
||||
import HomePage from '@/pages/home/HomePage.vue'
|
||||
import LoginPage from '@/pages/login/LoginPage.vue'
|
||||
import LaravelPage from '@/pages/testing/LaravelPage.vue'
|
||||
import WebsocketsPage from '@/pages/testing/WebsocketsPage.vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import Landing from '../pages/Landing.vue'
|
||||
import GameSelection from '../pages/GameSelection.vue'
|
||||
import GameBoard from '../pages/GameBoard.vue'
|
||||
import LeaderBoards from '../pages/LeaderBoards.vue'
|
||||
import Profile from '../pages/Profile.vue'
|
||||
import Login from '../pages/Login.vue'
|
||||
import Register from '../pages/Register.vue'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'Landing',
|
||||
component: Landing
|
||||
},
|
||||
{
|
||||
path: '/game-selection',
|
||||
name: 'GameSelection',
|
||||
component: GameSelection
|
||||
},
|
||||
{
|
||||
path: '/game-board',
|
||||
name: 'GameBoard',
|
||||
component: GameBoard
|
||||
},
|
||||
{
|
||||
path: '/leader-boards',
|
||||
name: 'LeaderBoards',
|
||||
component: LeaderBoards
|
||||
},
|
||||
{
|
||||
path: '/profile',
|
||||
name: 'Profile',
|
||||
component: Profile
|
||||
component: HomePage,
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'Login',
|
||||
component: Login
|
||||
component: LoginPage,
|
||||
},
|
||||
{
|
||||
path: '/register',
|
||||
name: 'Register',
|
||||
component: Register
|
||||
}
|
||||
path: '/testing',
|
||||
children: [
|
||||
{
|
||||
path: 'laravel',
|
||||
component: LaravelPage,
|
||||
},
|
||||
{
|
||||
path: 'websockets',
|
||||
component: WebsocketsPage,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
})
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import axios from 'axios'
|
||||
import { inject, ref } from 'vue'
|
||||
|
||||
export const useAPIStore = defineStore('api', () => {
|
||||
const API_BASE_URL = inject('apiBaseURL')
|
||||
|
||||
const token = ref()
|
||||
const gameQueryParameters = ref({
|
||||
page: 1,
|
||||
filters: {
|
||||
type: '',
|
||||
status: '',
|
||||
sort_by: 'began_at',
|
||||
sort_direction: 'desc',
|
||||
},
|
||||
})
|
||||
|
||||
// AUTH
|
||||
const postLogin = async (credentials) => {
|
||||
const response = await axios.post(`${API_BASE_URL}/login`, credentials)
|
||||
token.value = response.data.token
|
||||
axios.defaults.headers.common['Authorization'] = `Bearer ${token.value}`
|
||||
}
|
||||
const postLogout = async () => {
|
||||
await axios.post(`${API_BASE_URL}/logout`)
|
||||
token.value = undefined
|
||||
delete axios.defaults.headers.common['Authorization']
|
||||
}
|
||||
|
||||
// Users
|
||||
const getAuthUser = () => {
|
||||
return axios.get(`${API_BASE_URL}/users/me`)
|
||||
}
|
||||
|
||||
//Games
|
||||
const getGames = (resetPagination = false) => {
|
||||
if (resetPagination) {
|
||||
gameQueryParameters.value.page = 1
|
||||
}
|
||||
|
||||
const queryParams = new URLSearchParams({
|
||||
page: gameQueryParameters.value.page,
|
||||
...(gameQueryParameters.value.filters.type && {
|
||||
type: gameQueryParameters.value.filters.type,
|
||||
}),
|
||||
...(gameQueryParameters.value.filters.status && {
|
||||
status: gameQueryParameters.value.filters.status,
|
||||
}),
|
||||
sort_by: gameQueryParameters.value.filters.sort_by,
|
||||
sort_direction: gameQueryParameters.value.filters.sort_direction,
|
||||
}).toString()
|
||||
return axios.get(`${API_BASE_URL}/games?${queryParams}`)
|
||||
}
|
||||
|
||||
return {
|
||||
postLogin,
|
||||
postLogout,
|
||||
getAuthUser,
|
||||
getGames,
|
||||
gameQueryParameters,
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,37 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, computed } from 'vue'
|
||||
import { useAPIStore } from './api'
|
||||
|
||||
export const useAuthStore = defineStore('auth', () => {
|
||||
const apiStore = useAPIStore()
|
||||
|
||||
const currentUser = ref(undefined)
|
||||
|
||||
const isLoggedIn = computed(() => {
|
||||
return currentUser.value !== undefined
|
||||
})
|
||||
|
||||
const currentUserID = computed(() => {
|
||||
return currentUser.value?.id
|
||||
})
|
||||
|
||||
const login = async (credentials) => {
|
||||
await apiStore.postLogin(credentials)
|
||||
const response = await apiStore.getAuthUser()
|
||||
currentUser.value = response.data
|
||||
return response.data
|
||||
}
|
||||
|
||||
const logout = async () => {
|
||||
await apiStore.postLogout()
|
||||
currentUser.value = undefined
|
||||
}
|
||||
|
||||
return {
|
||||
currentUser,
|
||||
isLoggedIn,
|
||||
currentUserID,
|
||||
login,
|
||||
logout,
|
||||
}
|
||||
})
|
||||
@@ -1,12 +0,0 @@
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export const useCounterStore = defineStore('counter', () => {
|
||||
const count = ref(0)
|
||||
const doubleCount = computed(() => count.value * 2)
|
||||
function increment() {
|
||||
count.value++
|
||||
}
|
||||
|
||||
return { count, doubleCount, increment }
|
||||
})
|
||||