formatted, added team index, create, show
CI/CD / test_and_check_app (push) Failing after 48s
CI/CD / Publish immutable image (push) Skipped

This commit is contained in:
2026-09-19 01:09:55 +02:00
parent 9a025d6a80
commit b79d93fede
76 changed files with 3290 additions and 3146 deletions
+30 -4
View File
@@ -2,11 +2,13 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Enums\TeamMemberRole;
use App\Http\Requests\StoreTeamRequest; use App\Http\Requests\StoreTeamRequest;
use App\Http\Requests\UpdateTeamRequest; use App\Http\Requests\UpdateTeamRequest;
use App\Http\Resources\TeamResource; use App\Http\Resources\TeamResource;
use App\Models\Team; use App\Models\Team;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Inertia\Inertia; use Inertia\Inertia;
class TeamController extends Controller class TeamController extends Controller
@@ -17,13 +19,17 @@ class TeamController extends Controller
public function index(Request $request) public function index(Request $request)
{ {
return Inertia::render('team/Index', [ return Inertia::render('team/Index', [
'teams' => TeamResource::collection(Team::visibleTo($request->user())->get())->resolve($request), 'teams' => TeamResource::collection(Team::visibleTo(auth()->user())->get())->resolve($request),
'canCreateTeam' => auth()->user()->can('create', Team::class),
]); ]);
} }
public function create() public function show(Request$request, Team $team)
{ {
// return Inertia::render('team/Show', [
'team' => TeamResource::make($team)->resolve($request),
'canManageTeam' => auth()->user()->can('manage', $team),
]);
} }
/** /**
@@ -31,9 +37,29 @@ class TeamController extends Controller
*/ */
public function store(StoreTeamRequest $request) public function store(StoreTeamRequest $request)
{ {
// $validated = $request->validated();
try {
DB::transaction(function () use ($validated) {
$team = Team::create($validated);
$team->members()->attach(
request()->user()->id,
['role' => TeamMemberRole::Admin]
);
});
Inertia::flash('success', 'Team successfully created.');
return to_route('teams.index');
} catch (\Exception $exception) {
Inertia::flash('error', 'Team could not be created.');
DB::rollBack();
return back();
}
} }
public function create()
{
return Inertia::render('team/Create');
}
/** /**
* Show the form for editing the specified resource. * Show the form for editing the specified resource.
+10
View File
@@ -28,4 +28,14 @@ class StoreTeamRequest extends FormRequest
'slug' => Str::slug($this->name), 'slug' => Str::slug($this->name),
]); ]);
} }
public function messages(): array
{
return [
'slug.string' => '',
'slug.required' => '',
'slug.max' => '',
'slug.unique' => 'The name has already been taken.',
];
}
} }
@@ -18,6 +18,7 @@ class TeamMemberResource extends JsonResource
'email' => $this->email, 'email' => $this->email,
'has_multi_factor' => $this->hasVerifiedEmail() || $this->hasEnabledTwoFactorAuthentication(), 'has_multi_factor' => $this->hasVerifiedEmail() || $this->hasEnabledTwoFactorAuthentication(),
'role' => $this->pivot->role, 'role' => $this->pivot->role,
'joined_at' => $this->pivot->created_at->toDateTimeString(),
]; ];
} }
} }
+2 -1
View File
@@ -21,8 +21,9 @@ class TeamResource extends JsonResource
return [ return [
'name' => $this->name, 'name' => $this->name,
'slug' => $this->slug, 'slug' => $this->slug,
'members' => TeamMemberResource::collection($this->members), 'members' => TeamMemberResource::collection($this->members)->resolve($request),
'multi_factor_required' => $this->require_2fa, 'multi_factor_required' => $this->require_2fa,
'created_at' => $this->created_at->toDateTimeString(),
]; ];
} }
} }
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"$schema": "https://shadcn-vue.com/schema.json", "$schema": "https://shadcn-vue.com/schema.json",
"style": "new-york-v4", "style": "new-york",
"tailwind": { "tailwind": {
"config": "tailwind.config.js", "config": "tailwind.config.js",
"css": "resources/css/app.css", "css": "resources/css/app.css",
-5
View File
@@ -20,7 +20,6 @@
"require-dev": { "require-dev": {
"barryvdh/laravel-ide-helper": "^3.7", "barryvdh/laravel-ide-helper": "^3.7",
"fakerphp/faker": "^1.24", "fakerphp/faker": "^1.24",
"larastan/larastan": "^3.9",
"laravel/boost": "^2.2", "laravel/boost": "^2.2",
"laravel/pail": "^1.2.5", "laravel/pail": "^1.2.5",
"laravel/pao": "^1.0.6", "laravel/pao": "^1.0.6",
@@ -69,13 +68,9 @@
"pnpm run types:check", "pnpm run types:check",
"@test" "@test"
], ],
"types:check": [
"phpstan analyse --memory-limit=512M"
],
"test": [ "test": [
"@php artisan config:clear --ansi", "@php artisan config:clear --ansi",
"@lint:check", "@lint:check",
"@types:check",
"@php artisan test" "@php artisan test"
], ],
"post-autoload-dump": [ "post-autoload-dump": [
+1 -1
View File
@@ -7,7 +7,7 @@
"build:ssr": "vp build && vp build --ssr", "build:ssr": "vp build && vp build --ssr",
"dev": "vp dev", "dev": "vp dev",
"check": "vp check", "check": "vp check",
"check:fix": "vp check --fix", "format": "vp check --fix",
"types:check": "vue-tsc --noEmit" "types:check": "vue-tsc --noEmit"
}, },
"dependencies": { "dependencies": {
-13
View File
@@ -1,13 +0,0 @@
includes:
- vendor/larastan/larastan/extension.neon
- vendor/nesbot/carbon/extension.neon
parameters:
paths:
- app/
- bootstrap/app.php
- config/
- database/
- routes/
level: 7
+2 -3
View File
@@ -9,9 +9,8 @@
@theme inline { @theme inline {
--font-sans: --font-sans:
Instrument Sans, ui-sans-serif, system-ui, sans-serif, Instrument Sans, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
'Noto Color Emoji';
--radius-lg: var(--radius); --radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px); --radius-md: calc(var(--radius) - 2px);
+9 -34
View File
@@ -83,18 +83,12 @@ const rightNavItems: NavItem[] = [
<div class="lg:hidden"> <div class="lg:hidden">
<Sheet> <Sheet>
<SheetTrigger :as-child="true"> <SheetTrigger :as-child="true">
<Button <Button variant="ghost" size="icon" class="mr-2 h-9 w-9">
variant="ghost"
size="icon"
class="mr-2 h-9 w-9"
>
<Menu class="h-5 w-5" /> <Menu class="h-5 w-5" />
</Button> </Button>
</SheetTrigger> </SheetTrigger>
<SheetContent side="left" class="w-[300px] p-6"> <SheetContent side="left" class="w-[300px] p-6">
<SheetTitle class="sr-only" <SheetTitle class="sr-only">Navigation menu</SheetTitle>
>Navigation menu</SheetTitle
>
<SheetHeader class="flex justify-start text-left"> <SheetHeader class="flex justify-start text-left">
<AppLogoIcon <AppLogoIcon
class="size-6 fill-current text-black dark:text-white" class="size-6 fill-current text-black dark:text-white"
@@ -109,12 +103,7 @@ const rightNavItems: NavItem[] = [
:key="item.title" :key="item.title"
:href="item.href" :href="item.href"
class="hover:bg-accent flex items-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium" class="hover:bg-accent flex items-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium"
:class=" :class="whenCurrentUrl(item.href, activeItemStyles)"
whenCurrentUrl(
item.href,
activeItemStyles,
)
"
> >
<component <component
v-if="item.icon" v-if="item.icon"
@@ -153,9 +142,7 @@ const rightNavItems: NavItem[] = [
<!-- Desktop Menu --> <!-- Desktop Menu -->
<div class="hidden h-full lg:flex lg:flex-1"> <div class="hidden h-full lg:flex lg:flex-1">
<NavigationMenu class="ml-10 flex h-full items-stretch"> <NavigationMenu class="ml-10 flex h-full items-stretch">
<NavigationMenuList <NavigationMenuList class="flex h-full items-stretch space-x-2">
class="flex h-full items-stretch space-x-2"
>
<NavigationMenuItem <NavigationMenuItem
v-for="(item, index) in mainNavItems" v-for="(item, index) in mainNavItems"
:key="index" :key="index"
@@ -164,10 +151,7 @@ const rightNavItems: NavItem[] = [
<Link <Link
:class="[ :class="[
navigationMenuTriggerStyle(), navigationMenuTriggerStyle(),
whenCurrentUrl( whenCurrentUrl(item.href, activeItemStyles),
item.href,
activeItemStyles,
),
'h-9 cursor-pointer px-3', 'h-9 cursor-pointer px-3',
]" ]"
:href="item.href" :href="item.href"
@@ -195,16 +179,11 @@ const rightNavItems: NavItem[] = [
size="icon" size="icon"
class="group h-9 w-9 cursor-pointer" class="group h-9 w-9 cursor-pointer"
> >
<Search <Search class="size-5 opacity-80 group-hover:opacity-100" />
class="size-5 opacity-80 group-hover:opacity-100"
/>
</Button> </Button>
<div class="hidden space-x-1 lg:flex"> <div class="hidden space-x-1 lg:flex">
<template <template v-for="item in rightNavItems" :key="item.title">
v-for="item in rightNavItems"
:key="item.title"
>
<TooltipProvider :delay-duration="0"> <TooltipProvider :delay-duration="0">
<Tooltip> <Tooltip>
<TooltipTrigger> <TooltipTrigger>
@@ -219,9 +198,7 @@ const rightNavItems: NavItem[] = [
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
<span class="sr-only">{{ <span class="sr-only">{{ item.title }}</span>
item.title
}}</span>
<component <component
:is="item.icon" :is="item.icon"
class="size-5 opacity-80 group-hover:opacity-100" class="size-5 opacity-80 group-hover:opacity-100"
@@ -245,9 +222,7 @@ const rightNavItems: NavItem[] = [
size="icon" size="icon"
class="focus-within:ring-primary relative size-10 w-auto rounded-full p-1 focus-within:ring-2" class="focus-within:ring-primary relative size-10 w-auto rounded-full p-1 focus-within:ring-2"
> >
<Avatar <Avatar class="size-8 overflow-hidden rounded-full">
class="size-8 overflow-hidden rounded-full"
>
<AvatarImage <AvatarImage
v-if="auth.user.avatar" v-if="auth.user.avatar"
:src="auth.user.avatar" :src="auth.user.avatar"
+1 -3
View File
@@ -12,8 +12,6 @@ const name = usePage().props.name;
<AppLogoIcon class="size-5 fill-current text-white dark:text-black" /> <AppLogoIcon class="size-5 fill-current text-white dark:text-black" />
</div> </div>
<div class="ml-1 grid flex-1 text-left text-sm"> <div class="ml-1 grid flex-1 text-left text-sm">
<span class="mb-0.5 truncate leading-tight font-semibold">{{ <span class="mb-0.5 truncate leading-tight font-semibold">{{ name }}</span>
name
}}</span>
</div> </div>
</template> </template>
+2 -3
View File
@@ -16,7 +16,7 @@ import {
} from '@/components/ui/sidebar'; } from '@/components/ui/sidebar';
import { dashboard } from '@/routes'; import { dashboard } from '@/routes';
import type { NavItem } from '@/types'; import type { NavItem } from '@/types';
import teams from "@/routes/teams"; import teams from '@/routes/teams';
const mainNavItems: NavItem[] = [ const mainNavItems: NavItem[] = [
{ {
@@ -26,8 +26,7 @@ const mainNavItems: NavItem[] = [
}, },
]; ];
const footerNavItems: NavItem[] = [ const footerNavItems: NavItem[] = [];
];
</script> </script>
<template> <template>
+5 -10
View File
@@ -56,22 +56,17 @@ const passwordInput = useTemplateRef('passwordInput');
> >
<DialogHeader class="space-y-3"> <DialogHeader class="space-y-3">
<DialogTitle <DialogTitle
>Are you sure you want to delete your >Are you sure you want to delete your account?</DialogTitle
account?</DialogTitle
> >
<DialogDescription> <DialogDescription>
Once your account is deleted, all of its Once your account is deleted, all of its resources and data will
resources and data will also be permanently also be permanently deleted. Please enter your password to
deleted. Please enter your password to confirm confirm you would like to permanently delete your account.
you would like to permanently delete your
account.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div class="grid gap-2"> <div class="grid gap-2">
<Label for="password" class="sr-only" <Label for="password" class="sr-only">Password</Label>
>Password</Label
>
<PasswordInput <PasswordInput
id="password" id="password"
name="password" name="password"
+7 -14
View File
@@ -40,9 +40,9 @@ onUnmounted(() => clearTwoFactorAuthData());
class="flex flex-col items-start justify-start space-y-4" class="flex flex-col items-start justify-start space-y-4"
> >
<p class="text-muted-foreground text-sm"> <p class="text-muted-foreground text-sm">
When you enable two-factor authentication, you will be prompted When you enable two-factor authentication, you will be prompted for a
for a secure pin during login. This pin can be retrieved from a secure pin during login. This pin can be retrieved from a TOTP-supported
TOTP-supported application on your phone. application on your phone.
</p> </p>
<div> <div>
@@ -55,27 +55,20 @@ onUnmounted(() => clearTwoFactorAuthData());
@success="showSetupModal = true" @success="showSetupModal = true"
#default="{ processing }" #default="{ processing }"
> >
<Button type="submit" :disabled="processing"> <Button type="submit" :disabled="processing"> Enable 2FA </Button>
Enable 2FA
</Button>
</Form> </Form>
</div> </div>
</div> </div>
<div v-else class="flex flex-col items-start justify-start space-y-4"> <div v-else class="flex flex-col items-start justify-start space-y-4">
<p class="text-muted-foreground text-sm"> <p class="text-muted-foreground text-sm">
You will be prompted for a secure, random pin during login, You will be prompted for a secure, random pin during login, which you
which you can retrieve from the TOTP-supported application on can retrieve from the TOTP-supported application on your phone.
your phone.
</p> </p>
<div class="relative inline"> <div class="relative inline">
<Form v-bind="disable.form()" #default="{ processing }"> <Form v-bind="disable.form()" #default="{ processing }">
<Button <Button variant="destructive" type="submit" :disabled="processing">
variant="destructive"
type="submit"
:disabled="processing"
>
Disable 2FA Disable 2FA
</Button> </Button>
</Form> </Form>
+1 -5
View File
@@ -38,11 +38,7 @@ const { isMobile, state } = useSidebar();
<DropdownMenuContent <DropdownMenuContent
class="w-(--reka-dropdown-menu-trigger-width) min-w-56 rounded-lg" class="w-(--reka-dropdown-menu-trigger-width) min-w-56 rounded-lg"
:side=" :side="
isMobile isMobile ? 'bottom' : state === 'collapsed' ? 'left' : 'bottom'
? 'bottom'
: state === 'collapsed'
? 'left'
: 'bottom'
" "
align="end" align="end"
:side-offset="4" :side-offset="4"
+2 -2
View File
@@ -74,8 +74,8 @@ const handleDelete = () => {
<DialogContent> <DialogContent>
<DialogTitle>Remove passkey</DialogTitle> <DialogTitle>Remove passkey</DialogTitle>
<DialogDescription> <DialogDescription>
Are you sure you want to remove the "{{ passkey.name }}" Are you sure you want to remove the "{{ passkey.name }}" passkey? You
passkey? You will no longer be able to use it to sign in. will no longer be able to use it to sign in.
</DialogDescription> </DialogDescription>
<DialogFooter class="gap-2"> <DialogFooter class="gap-2">
<DialogClose as-child> <DialogClose as-child>
@@ -45,8 +45,8 @@ onMounted(async () => {
<LockKeyhole class="size-4" />2FA recovery codes <LockKeyhole class="size-4" />2FA recovery codes
</CardTitle> </CardTitle>
<CardDescription> <CardDescription>
Recovery codes let you regain access if you lose your 2FA Recovery codes let you regain access if you lose your 2FA device. Store
device. Store them in a secure password manager. them in a secure password manager.
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@@ -58,8 +58,7 @@ onMounted(async () => {
:is="isRecoveryCodesVisible ? EyeOff : Eye" :is="isRecoveryCodesVisible ? EyeOff : Eye"
class="size-4" class="size-4"
/> />
{{ isRecoveryCodesVisible ? 'Hide' : 'View' }} recovery {{ isRecoveryCodesVisible ? 'Hide' : 'View' }} recovery codes
codes
</Button> </Button>
<Form <Form
@@ -70,11 +69,7 @@ onMounted(async () => {
@success="fetchRecoveryCodes" @success="fetchRecoveryCodes"
#default="{ processing }" #default="{ processing }"
> >
<Button <Button variant="secondary" type="submit" :disabled="processing">
variant="secondary"
type="submit"
:disabled="processing"
>
<RefreshCw /> Regenerate codes <RefreshCw /> Regenerate codes
</Button> </Button>
</Form> </Form>
@@ -82,9 +77,7 @@ onMounted(async () => {
<div <div
:class="[ :class="[
'relative overflow-hidden transition-all duration-300', 'relative overflow-hidden transition-all duration-300',
isRecoveryCodesVisible isRecoveryCodesVisible ? 'h-auto opacity-100' : 'h-0 opacity-0',
? 'h-auto opacity-100'
: 'h-0 opacity-0',
]" ]"
> >
<div v-if="errors?.length" class="mt-6"> <div v-if="errors?.length" class="mt-6">
@@ -102,18 +95,13 @@ onMounted(async () => {
class="bg-muted-foreground/20 h-4 animate-pulse rounded" class="bg-muted-foreground/20 h-4 animate-pulse rounded"
></div> ></div>
</div> </div>
<div <div v-else v-for="(code, index) in recoveryCodesList" :key="index">
v-else
v-for="(code, index) in recoveryCodesList"
:key="index"
>
{{ code }} {{ code }}
</div> </div>
</div> </div>
<p class="text-muted-foreground text-xs select-none"> <p class="text-muted-foreground text-xs select-none">
Each recovery code can be used once to access your Each recovery code can be used once to access your account and will
account and will be removed after use. If you need more, be removed after use. If you need more, click
click
<span class="font-bold">Regenerate codes</span> above. <span class="font-bold">Regenerate codes</span> above.
</p> </p>
</div> </div>
@@ -119,27 +119,21 @@ watch(
<div <div
class="border-border bg-muted relative overflow-hidden rounded-full border p-2.5" class="border-border bg-muted relative overflow-hidden rounded-full border p-2.5"
> >
<div <div class="absolute inset-0 grid grid-cols-5 opacity-50">
class="absolute inset-0 grid grid-cols-5 opacity-50"
>
<div <div
v-for="i in 5" v-for="i in 5"
:key="`col-${i}`" :key="`col-${i}`"
class="border-border border-r last:border-r-0" class="border-border border-r last:border-r-0"
/> />
</div> </div>
<div <div class="absolute inset-0 grid grid-rows-5 opacity-50">
class="absolute inset-0 grid grid-rows-5 opacity-50"
>
<div <div
v-for="i in 5" v-for="i in 5"
:key="`row-${i}`" :key="`row-${i}`"
class="border-border border-b last:border-b-0" class="border-border border-b last:border-b-0"
/> />
</div> </div>
<ScanLine <ScanLine class="text-foreground relative z-20 size-6" />
class="text-foreground relative z-20 size-6"
/>
</div> </div>
</div> </div>
<DialogTitle>{{ modalConfig.title }}</DialogTitle> <DialogTitle>{{ modalConfig.title }}</DialogTitle>
@@ -166,10 +160,7 @@ watch(
> >
<Spinner class="size-6" /> <Spinner class="size-6" />
</div> </div>
<div <div v-else class="relative z-10 overflow-hidden border p-5">
v-else
class="relative z-10 overflow-hidden border p-5"
>
<div <div
v-html="qrCodeSvg" v-html="qrCodeSvg"
class="flex aspect-square size-full items-center justify-center" class="flex aspect-square size-full items-center justify-center"
@@ -190,20 +181,14 @@ watch(
</Button> </Button>
</div> </div>
<div <div class="relative flex w-full items-center justify-center">
class="relative flex w-full items-center justify-center" <div class="bg-border absolute inset-0 top-1/2 h-px w-full" />
>
<div
class="bg-border absolute inset-0 top-1/2 h-px w-full"
/>
<span class="bg-card relative px-2 py-1" <span class="bg-card relative px-2 py-1"
>or, enter the code manually</span >or, enter the code manually</span
> >
</div> </div>
<div <div class="flex w-full items-center justify-center space-x-2">
class="flex w-full items-center justify-center space-x-2"
>
<div <div
class="border-border flex w-full items-stretch overflow-hidden rounded-xl border" class="border-border flex w-full items-stretch overflow-hidden rounded-xl border"
> >
@@ -224,10 +209,7 @@ watch(
@click="copy(manualSetupKey || '')" @click="copy(manualSetupKey || '')"
class="border-border hover:bg-muted relative block h-auto border-l px-3" class="border-border hover:bg-muted relative block h-auto border-l px-3"
> >
<Check <Check v-if="copied" class="w-4 text-green-500" />
v-if="copied"
class="w-4 text-green-500"
/>
<Copy v-else class="w-4" /> <Copy v-else class="w-4" />
</button> </button>
</template> </template>
@@ -246,10 +228,7 @@ watch(
v-slot="{ errors, processing }" v-slot="{ errors, processing }"
> >
<input type="hidden" name="code" :value="code" /> <input type="hidden" name="code" :value="code" />
<div <div ref="pinInputContainerRef" class="relative w-full space-y-3">
ref="pinInputContainerRef"
class="relative w-full space-y-3"
>
<div <div
class="flex w-full flex-col items-center justify-center space-y-3 py-2" class="flex w-full flex-col items-center justify-center space-y-3 py-2"
> >
+2 -7
View File
@@ -16,15 +16,10 @@ export function updateTheme(value: Appearance): void {
} }
if (value === 'system') { if (value === 'system') {
const mediaQueryList = window.matchMedia( const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
'(prefers-color-scheme: dark)',
);
const systemTheme = mediaQueryList.matches ? 'dark' : 'light'; const systemTheme = mediaQueryList.matches ? 'dark' : 'light';
document.documentElement.classList.toggle( document.documentElement.classList.toggle('dark', systemTheme === 'dark');
'dark',
systemTheme === 'dark',
);
} else { } else {
document.documentElement.classList.toggle('dark', value === 'dark'); document.documentElement.classList.toggle('dark', value === 'dark');
} }
+1 -3
View File
@@ -26,9 +26,7 @@ defineProps<{
class="flex items-center gap-2 self-center font-medium" class="flex items-center gap-2 self-center font-medium"
> >
<div class="flex h-9 w-9 items-center justify-center"> <div class="flex h-9 w-9 items-center justify-center">
<AppLogoIcon <AppLogoIcon class="size-9 fill-current text-black dark:text-white" />
class="size-9 fill-current text-black dark:text-white"
/>
</div> </div>
</Link> </Link>
+1 -4
View File
@@ -37,10 +37,7 @@ const { isCurrentOrParentUrl } = useCurrentUrl();
<div class="flex flex-col lg:flex-row lg:space-x-12"> <div class="flex flex-col lg:flex-row lg:space-x-12">
<aside class="w-full max-w-xl lg:w-48"> <aside class="w-full max-w-xl lg:w-48">
<nav <nav class="flex flex-col space-y-1 space-x-0" aria-label="Settings">
class="flex flex-col space-y-1 space-x-0"
aria-label="Settings"
>
<Button <Button
v-for="item in sidebarNavItems" v-for="item in sidebarNavItems"
:key="toUrl(item.href)" :key="toUrl(item.href)"
+1 -1
View File
@@ -1,6 +1,6 @@
import type { InertiaLinkProps } from '@inertiajs/vue3'; import type { InertiaLinkProps } from '@inertiajs/vue3';
import { clsx } from 'clsx';
import type { ClassValue } from 'clsx'; import type { ClassValue } from 'clsx';
import { clsx } from 'clsx';
import { twMerge } from 'tailwind-merge'; import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
+1 -3
View File
@@ -18,9 +18,7 @@ defineOptions({
<template> <template>
<Head title="Dashboard" /> <Head title="Dashboard" />
<div <div class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4">
class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4"
>
<div class="grid auto-rows-min gap-4 md:grid-cols-3"> <div class="grid auto-rows-min gap-4 md:grid-cols-3">
<div <div
class="border-sidebar-border/70 dark:border-sidebar-border relative aspect-video overflow-hidden rounded-xl border" class="border-sidebar-border/70 dark:border-sidebar-border relative aspect-video overflow-hidden rounded-xl border"
+7 -22
View File
@@ -50,16 +50,14 @@ import { register } from '@/routes';
> >
<h1 class="mb-1 font-medium">Let's get started</h1> <h1 class="mb-1 font-medium">Let's get started</h1>
<p class="mb-2 text-[#706f6c] dark:text-[#A1A09A]"> <p class="mb-2 text-[#706f6c] dark:text-[#A1A09A]">
Laravel has an incredibly rich ecosystem. <br />We Laravel has an incredibly rich ecosystem. <br />We suggest starting
suggest starting with the following. with the following.
</p> </p>
<ul class="mb-4 flex flex-col lg:mb-6"> <ul class="mb-4 flex flex-col lg:mb-6">
<li <li
class="relative flex items-center gap-4 py-2 before:absolute before:top-1/2 before:bottom-0 before:left-[0.4rem] before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A]" class="relative flex items-center gap-4 py-2 before:absolute before:top-1/2 before:bottom-0 before:left-[0.4rem] before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A]"
> >
<span <span class="relative bg-white py-1 dark:bg-[#161615]">
class="relative bg-white py-1 dark:bg-[#161615]"
>
<span <span
class="flex h-3.5 w-3.5 items-center justify-center rounded-full border border-[#e3e3e0] bg-[#FDFDFC] shadow-[0px_0px_1px_0px_rgba(0,0,0,0.03),0px_1px_2px_0px_rgba(0,0,0,0.06)] dark:border-[#3E3E3A] dark:bg-[#161615]" class="flex h-3.5 w-3.5 items-center justify-center rounded-full border border-[#e3e3e0] bg-[#FDFDFC] shadow-[0px_0px_1px_0px_rgba(0,0,0,0.03),0px_1px_2px_0px_rgba(0,0,0,0.06)] dark:border-[#3E3E3A] dark:bg-[#161615]"
> >
@@ -96,9 +94,7 @@ import { register } from '@/routes';
<li <li
class="relative flex items-center gap-4 py-2 before:absolute before:top-0 before:bottom-1/2 before:left-[0.4rem] before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A]" class="relative flex items-center gap-4 py-2 before:absolute before:top-0 before:bottom-1/2 before:left-[0.4rem] before:border-l before:border-[#e3e3e0] dark:before:border-[#3E3E3A]"
> >
<span <span class="relative bg-white py-1 dark:bg-[#161615]">
class="relative bg-white py-1 dark:bg-[#161615]"
>
<span <span
class="flex h-3.5 w-3.5 items-center justify-center rounded-full border border-[#e3e3e0] bg-[#FDFDFC] shadow-[0px_0px_1px_0px_rgba(0,0,0,0.03),0px_1px_2px_0px_rgba(0,0,0,0.06)] dark:border-[#3E3E3A] dark:bg-[#161615]" class="flex h-3.5 w-3.5 items-center justify-center rounded-full border border-[#e3e3e0] bg-[#FDFDFC] shadow-[0px_0px_1px_0px_rgba(0,0,0,0.03),0px_1px_2px_0px_rgba(0,0,0,0.06)] dark:border-[#3E3E3A] dark:bg-[#161615]"
> >
@@ -167,10 +163,7 @@ import { register } from '@/routes';
d="M242.805 41.6337C240.611 38.1275 237.494 35.3731 233.455 33.3681C229.416 31.3647 225.351 30.3618 221.262 30.3618C215.974 30.3618 211.138 31.3389 206.75 33.2923C202.36 35.2456 198.597 37.928 195.455 41.3333C192.314 44.7401 189.869 48.6726 188.125 53.1293C186.378 57.589 185.507 62.274 185.507 67.1813C185.507 72.1925 186.378 76.8995 188.125 81.3069C189.868 85.7173 192.313 89.6241 195.455 93.0293C198.597 96.4361 202.361 99.1155 206.75 101.069C211.138 103.022 215.974 103.999 221.262 103.999C225.351 103.999 229.416 102.997 233.455 100.994C237.494 98.9911 240.611 96.2359 242.805 92.7282V102.195H259.112V32.1642H242.805V41.6337ZM241.31 75.7472C240.312 78.4531 238.916 80.8078 237.122 82.8112C235.326 84.8161 233.156 86.4183 230.614 87.6206C228.07 88.823 225.251 89.4238 222.16 89.4238C219.068 89.4238 216.275 88.823 213.782 87.6206C211.289 86.4183 209.145 84.8161 207.35 82.8112C205.554 80.8078 204.182 78.4531 203.236 75.7472C202.286 73.0421 201.814 70.1868 201.814 67.1821C201.814 64.1758 202.286 61.3205 203.236 58.6154C204.182 55.9102 205.554 53.5571 207.35 51.5522C209.145 49.5495 211.288 47.9451 213.782 46.7427C216.275 45.5404 219.068 44.9396 222.16 44.9396C225.251 44.9396 228.07 45.5404 230.614 46.7427C233.156 47.9451 235.326 49.5487 237.122 51.5522C238.917 53.5571 240.312 55.9102 241.31 58.6154C242.306 61.3205 242.806 64.1758 242.806 67.1821C242.805 70.1868 242.305 73.0421 241.31 75.7472Z" d="M242.805 41.6337C240.611 38.1275 237.494 35.3731 233.455 33.3681C229.416 31.3647 225.351 30.3618 221.262 30.3618C215.974 30.3618 211.138 31.3389 206.75 33.2923C202.36 35.2456 198.597 37.928 195.455 41.3333C192.314 44.7401 189.869 48.6726 188.125 53.1293C186.378 57.589 185.507 62.274 185.507 67.1813C185.507 72.1925 186.378 76.8995 188.125 81.3069C189.868 85.7173 192.313 89.6241 195.455 93.0293C198.597 96.4361 202.361 99.1155 206.75 101.069C211.138 103.022 215.974 103.999 221.262 103.999C225.351 103.999 229.416 102.997 233.455 100.994C237.494 98.9911 240.611 96.2359 242.805 92.7282V102.195H259.112V32.1642H242.805V41.6337ZM241.31 75.7472C240.312 78.4531 238.916 80.8078 237.122 82.8112C235.326 84.8161 233.156 86.4183 230.614 87.6206C228.07 88.823 225.251 89.4238 222.16 89.4238C219.068 89.4238 216.275 88.823 213.782 87.6206C211.289 86.4183 209.145 84.8161 207.35 82.8112C205.554 80.8078 204.182 78.4531 203.236 75.7472C202.286 73.0421 201.814 70.1868 201.814 67.1821C201.814 64.1758 202.286 61.3205 203.236 58.6154C204.182 55.9102 205.554 53.5571 207.35 51.5522C209.145 49.5495 211.288 47.9451 213.782 46.7427C216.275 45.5404 219.068 44.9396 222.16 44.9396C225.251 44.9396 228.07 45.5404 230.614 46.7427C233.156 47.9451 235.326 49.5487 237.122 51.5522C238.917 53.5571 240.312 55.9102 241.31 58.6154C242.306 61.3205 242.806 64.1758 242.806 67.1821C242.805 70.1868 242.305 73.0421 241.31 75.7472Z"
fill="currentColor" fill="currentColor"
/> />
<path <path d="M438 -3H421.694V102.197H438V-3Z" fill="currentColor" />
d="M438 -3H421.694V102.197H438V-3Z"
fill="currentColor"
/>
<path <path
d="M139.43 102.197H155.735V48.2834H183.712V32.1665H139.43V102.197Z" d="M139.43 102.197H155.735V48.2834H183.712V32.1665H139.43V102.197Z"
fill="currentColor" fill="currentColor"
@@ -300,19 +293,11 @@ import { register } from '@/routes';
height="299" height="299"
fill="black" fill="black"
> >
<rect <rect fill="white" x="51" y="103" width="338" height="299" />
fill="white"
x="51"
y="103"
width="338"
height="299"
/>
<path <path
d="M286.264 400.8C255.464 400.8 230.264 392.4 210.664 375.6C191.464 358.8 181.864 337 181.864 310.2H252.064C252.064 318.2 255.064 324.8 261.064 330C267.064 335.2 275.064 337.8 285.064 337.8C294.664 337.8 302.464 335 308.464 329.4C314.864 323.8 318.064 316.6 318.064 307.8C318.064 299.8 315.264 293.2 309.664 288C304.064 282.8 296.864 280.2 288.064 280.2H250.864V218.4H288.064C294.864 218.4 300.664 216 305.464 211.2C310.264 206.4 312.664 200.4 312.664 193.2C312.664 184.8 310.064 178.2 304.864 173.4C299.664 168.6 293.064 166.2 285.064 166.2C277.864 166.2 271.664 168.4 266.464 172.8C261.664 177.2 259.264 182.8 259.264 189.6H192.664C192.664 164.8 201.464 144.6 219.064 129C236.664 113 259.264 105 286.864 105C314.464 105 336.864 112.2 354.064 126.6C371.664 141 380.464 160 380.464 183.6C380.464 200.8 375.864 214.8 366.664 225.6C357.464 236 345.664 243.2 331.264 247.2C348.464 252 362.064 260.2 372.064 271.8C382.464 283.4 387.664 298 387.664 315.6C387.664 340.4 378.264 360.8 359.464 376.8C340.664 392.8 316.264 400.8 286.264 400.8Z" d="M286.264 400.8C255.464 400.8 230.264 392.4 210.664 375.6C191.464 358.8 181.864 337 181.864 310.2H252.064C252.064 318.2 255.064 324.8 261.064 330C267.064 335.2 275.064 337.8 285.064 337.8C294.664 337.8 302.464 335 308.464 329.4C314.864 323.8 318.064 316.6 318.064 307.8C318.064 299.8 315.264 293.2 309.664 288C304.064 282.8 296.864 280.2 288.064 280.2H250.864V218.4H288.064C294.864 218.4 300.664 216 305.464 211.2C310.264 206.4 312.664 200.4 312.664 193.2C312.664 184.8 310.064 178.2 304.864 173.4C299.664 168.6 293.064 166.2 285.064 166.2C277.864 166.2 271.664 168.4 266.464 172.8C261.664 177.2 259.264 182.8 259.264 189.6H192.664C192.664 164.8 201.464 144.6 219.064 129C236.664 113 259.264 105 286.864 105C314.464 105 336.864 112.2 354.064 126.6C371.664 141 380.464 160 380.464 183.6C380.464 200.8 375.864 214.8 366.664 225.6C357.464 236 345.664 243.2 331.264 247.2C348.464 252 362.064 260.2 372.064 271.8C382.464 283.4 387.664 298 387.664 315.6C387.664 340.4 378.264 360.8 359.464 376.8C340.664 392.8 316.264 400.8 286.264 400.8Z"
/> />
<path <path d="M78.2 167.6H53V105.2H146V400.2H78.2V167.6Z" />
d="M78.2 167.6H53V105.2H146V400.2H78.2V167.6Z"
/>
</mask> </mask>
<path <path
d="M286.264 400.8C255.464 400.8 230.264 392.4 210.664 375.6C191.464 358.8 181.864 337 181.864 310.2H252.064C252.064 318.2 255.064 324.8 261.064 330C267.064 335.2 275.064 337.8 285.064 337.8C294.664 337.8 302.464 335 308.464 329.4C314.864 323.8 318.064 316.6 318.064 307.8C318.064 299.8 315.264 293.2 309.664 288C304.064 282.8 296.864 280.2 288.064 280.2H250.864V218.4H288.064C294.864 218.4 300.664 216 305.464 211.2C310.264 206.4 312.664 200.4 312.664 193.2C312.664 184.8 310.064 178.2 304.864 173.4C299.664 168.6 293.064 166.2 285.064 166.2C277.864 166.2 271.664 168.4 266.464 172.8C261.664 177.2 259.264 182.8 259.264 189.6H192.664C192.664 164.8 201.464 144.6 219.064 129C236.664 113 259.264 105 286.864 105C314.464 105 336.864 112.2 354.064 126.6C371.664 141 380.464 160 380.464 183.6C380.464 200.8 375.864 214.8 366.664 225.6C357.464 236 345.664 243.2 331.264 247.2C348.464 252 362.064 260.2 372.064 271.8C382.464 283.4 387.664 298 387.664 315.6C387.664 340.4 378.264 360.8 359.464 376.8C340.664 392.8 316.264 400.8 286.264 400.8Z" d="M286.264 400.8C255.464 400.8 230.264 392.4 210.664 375.6C191.464 358.8 181.864 337 181.864 310.2H252.064C252.064 318.2 255.064 324.8 261.064 330C267.064 335.2 275.064 337.8 285.064 337.8C294.664 337.8 302.464 335 308.464 329.4C314.864 323.8 318.064 316.6 318.064 307.8C318.064 299.8 315.264 293.2 309.664 288C304.064 282.8 296.864 280.2 288.064 280.2H250.864V218.4H288.064C294.864 218.4 300.664 216 305.464 211.2C310.264 206.4 312.664 200.4 312.664 193.2C312.664 184.8 310.064 178.2 304.864 173.4C299.664 168.6 293.064 166.2 285.064 166.2C277.864 166.2 271.664 168.4 266.464 172.8C261.664 177.2 259.264 182.8 259.264 189.6H192.664C192.664 164.8 201.464 144.6 219.064 129C236.664 113 259.264 105 286.864 105C314.464 105 336.864 112.2 354.064 126.6C371.664 141 380.464 160 380.464 183.6C380.464 200.8 375.864 214.8 366.664 225.6C357.464 236 345.664 243.2 331.264 247.2C348.464 252 362.064 260.2 372.064 271.8C382.464 283.4 387.664 298 387.664 315.6C387.664 340.4 378.264 360.8 359.464 376.8C340.664 392.8 316.264 400.8 286.264 400.8Z"
+1 -5
View File
@@ -34,11 +34,7 @@ defineOptions({
separator="Or confirm with password" separator="Or confirm with password"
/> />
<Form <Form v-bind="store.form()" reset-on-success v-slot="{ errors, processing }">
v-bind="store.form()"
reset-on-success
v-slot="{ errors, processing }"
>
<div class="space-y-6"> <div class="space-y-6">
<div class="grid gap-2"> <div class="grid gap-2">
<Label htmlFor="password">Password</Label> <Label htmlFor="password">Password</Label>
+2 -8
View File
@@ -97,10 +97,7 @@ defineOptions({
</div> </div>
<div class="flex items-center gap-4"> <div class="flex items-center gap-4">
<Button <Button :disabled="processing" data-test="update-password-button">
:disabled="processing"
data-test="update-password-button"
>
Save Save
</Button> </Button>
</div> </div>
@@ -113,8 +110,5 @@ defineOptions({
:twoFactorEnabled="twoFactorEnabled" :twoFactorEnabled="twoFactorEnabled"
/> />
<ManagePasskeys <ManagePasskeys :canManagePasskeys="canManagePasskeys" :passkeys="passkeys" />
:canManagePasskeys="canManagePasskeys"
:passkeys="passkeys"
/>
</template> </template>
+59 -4
View File
@@ -1,11 +1,66 @@
<script setup lang="ts"> <script setup lang="ts">
import teamRoutes from '@/routes/teams';
import { Save } from '@lucide/vue';
import { Head, useForm } from '@inertiajs/vue3';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import InputError from '@/components/InputError.vue';
import { Checkbox } from '@/components/ui/checkbox';
defineOptions({
layout: {
breadcrumbs: [
{
title: 'Teams',
href: teamRoutes.index(),
},
{
title: 'Erstellen',
href: teamRoutes.create(),
},
],
},
});
const form = useForm({
name: '',
require_2fa: false,
});
function submit(): void {
form.submit(teamRoutes.store());
}
</script> </script>
<template> <template>
<Head title="Teams" />
<div class="flex flex-col justify-center p-16">
<form class="grid w-full gap-5 py-4" @submit.prevent="submit">
<div class="grid grid-cols-2 gap-5">
<div class="grid items-start gap-3">
<Label for="name">Name</Label>
<Input id="name" name="name" v-model="form.name" />
<InputError :message="form.errors.name" />
<InputError :message="(form.errors as any).slug" />
</div>
<div class="grid items-start gap-3">
<Label for="require_2fa">Require 2FA</Label>
<Checkbox
id="require_2fa"
name="require_2fa"
v-model="form.require_2fa"
/>
<InputError :message="form.errors.require_2fa" />
</div>
</div>
<div class="flex w-full justify-end">
<Button type="submit" class="w-fit" :disabled="form.processing">
<component :is="Save" />
Erstellen
</Button>
</div>
</form>
</div>
</template> </template>
<style scoped>
</style>
+65 -5
View File
@@ -1,11 +1,71 @@
<script setup lang="ts"> <script lang="ts" setup>
import teamRoutes from '@/routes/teams';
import { Save } from '@lucide/vue';
import { Head, useForm } from '@inertiajs/vue3';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import InputError from '@/components/InputError.vue';
import { Checkbox } from '@/components/ui/checkbox';
import { Team } from '@/types/models';
const props = defineProps<{
team: Team;
}>();
defineOptions({
layout: {
breadcrumbs: [
{
title: 'Teams',
href: teamRoutes.index(),
},
{
title: props.team.name,
href: teamRoutes.create(),
},
],
},
});
const form = useForm({
name: props.team.name,
require_2fa: props.team.multi_factor_required,
});
function submit(): void {
form.patch(teamRoutes.patch({ team: props.team.slug }).url);
}
</script> </script>
<template> <template>
<Head title="Teams" />
<div class="flex flex-col justify-center p-16">
<form class="grid w-full gap-5 py-4" @submit.prevent="submit">
<div class="grid grid-cols-2 gap-5">
<div class="grid items-start gap-3">
<Label for="name">Name</Label>
<Input id="name" v-model="form.name" name="name" />
<InputError :message="form.errors.name" />
<InputError :message="(form.errors as any).slug" />
</div>
<div class="grid items-start gap-3">
<Label for="require_2fa">Require 2FA</Label>
<Checkbox
id="require_2fa"
v-model="form.require_2fa"
name="require_2fa"
/>
<InputError :message="form.errors.require_2fa" />
</div>
</div>
<div class="flex w-full justify-end">
<Button :disabled="form.processing" class="w-fit" type="submit">
<component :is="Save" />
Speichern
</Button>
</div>
</form>
</div>
</template> </template>
<style scoped>
</style>
+47 -25
View File
@@ -1,25 +1,18 @@
<script setup lang="ts"> <script lang="ts" setup>
import teamRoutes from '@/routes/teams';
import teamRoutes from "@/routes/teams"; import {Head, Link, router, usePage} from '@inertiajs/vue3';
import {Head} from "@inertiajs/vue3"; import type {Team} from '@/types/models';
import type {Team} from "@/types/models"; import TextLink from '@/components/TextLink.vue';
import TextLink from "@/components/TextLink.vue"; import {Card, CardContent} from '@/components/ui/card';
import {Card, CardContent} from "@/components/ui/card"; import {Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
import { import {Button} from '@/components/ui/button';
Table, import {Eye, Pencil, Plus} from '@lucide/vue';
TableBody,
TableCaption,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
defineOptions({ defineOptions({
layout: { layout: {
breadcrumbs: [ breadcrumbs: [
{ {
title: 'Meine Teams', title: 'Teams',
href: teamRoutes.index(), href: teamRoutes.index(),
}, },
], ],
@@ -27,14 +20,31 @@ defineOptions({
}); });
defineProps<{ defineProps<{
teams: Team[] teams: Team[];
}>() canCreateTeam: boolean;
}>();
const user = usePage().props.auth.user;
const openTeam = (team: Team) =>
router.visit(teamRoutes.show({team: team.slug}).url);
const canManageTeam = (team: Team) =>
team.members.find((member) => member.email === user.email)?.role === 'admin';
</script> </script>
<template> <template>
<Head title="Teams"/> <Head title="Teams"/>
<div class="flex justify-center p-16"> <div class="flex flex-col justify-center p-16">
<div v-if="canCreateTeam" class="flex w-full justify-end py-4">
<Link :href="teamRoutes.create()">
<Button>
<component :is="Plus"/>
Erstellen
</Button>
</Link>
</div>
<Card class="w-full"> <Card class="w-full">
<CardContent> <CardContent>
@@ -49,19 +59,31 @@ defineProps<{
</TableHeader> </TableHeader>
<TableBody> <TableBody>
<TableRow <TableRow
v-if="teams.length > 0"
v-for="team in teams" v-for="team in teams"
v-if="teams.length > 0"
:key="team.slug"
role="link"
tabindex="0"
@click="openTeam(team)"
> >
<TableCell>{{ team.name }}</TableCell> <TableCell>{{ team.name }}</TableCell>
<TableCell>{{ team.slug }}</TableCell> <TableCell>{{ team.slug }}</TableCell>
<TableCell>{{ team.members.length }}</TableCell> <TableCell>{{ team.members.length }}</TableCell>
<TableCell>{{ team.multi_factor_required }}</TableCell> <TableCell>{{ team.multi_factor_required ? 'Ja' : 'Nein' }}</TableCell>
<Link
:href="(canManageTeam(team) ? teamRoutes.edit : teamRoutes.show)({ team: team.slug })"
class="cursor-pointer"
@click.stop
>
<TableCell>
<component :is="canManageTeam(team) ? Pencil : Eye" size="18"/>
</TableCell>
</Link>
</TableRow> </TableRow>
</TableBody> </TableBody>
<TableCaption v-if="teams.length <= 0"> <TableCaption v-if="teams.length <= 0">
Du bist noch von keinem Team mitglied. Du bist noch von keinem Team mitglied. Tritt einem Team bei oder
Tritt einem Team bei oder <TextLink :href="teamRoutes.create()">erstelle eins.</TextLink>
<TextLink href="{{ route('teams.create') }}">erstelle eins.</TextLink>
</TableCaption> </TableCaption>
</Table> </Table>
</CardContent> </CardContent>
+90
View File
@@ -0,0 +1,90 @@
<script lang="ts" setup>
import teamRoutes from '@/routes/teams';
import {Head, Link, setLayoutProps} from '@inertiajs/vue3';
import type {Team} from '@/types/models';
import {Card, CardContent} from '@/components/ui/card';
import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
import {Button} from '@/components/ui/button';
import {Pencil} from '@lucide/vue';
const props = defineProps<{
team: Team;
canManageTeam: boolean;
}>();
setLayoutProps({
breadcrumbs: [
{
title: 'Teams',
href: teamRoutes.index(),
},
{
title: props.team.name,
href: teamRoutes.index(),
},
],
});
</script>
<template>
<Head title="Teams"/>
<div class="flex flex-col justify-center p-16">
<div v-if="canManageTeam" class="flex w-full justify-end py-4">
<Link :href="teamRoutes.create()">
<Button>
<component :is="Pencil"/>
Bearbeiten
</Button>
</Link>
</div>
<Card class="w-full mb-8">
<CardContent class="flex flex-col">
<h1 class="font-semibold text-2xl">{{ team.name }}</h1>
<Table>
<TableBody class="[&_td]:py-0 [&_tr]:border-0 text-gray-500 text-sm">
<TableRow>
<TableCell class="font-bold ">Slug:</TableCell>
<TableCell>{{ team.slug }}</TableCell>
</TableRow>
<TableRow>
<TableCell class="font-bold ">Erfordert 2FA:</TableCell>
<TableCell>{{ team.multi_factor_required ? 'Ja' : 'Nein' }}</TableCell>
</TableRow>
<TableRow>
<TableCell class="font-bold ">Erstellt am:</TableCell>
<TableCell>{{ team.created_at }}</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardContent>
<h2 class="font-semibold text-lg">Mitglieder</h2>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Email</TableHead>
<TableHead>2FA</TableHead>
<TableHead>Rolle</TableHead>
<TableHead>Beigetreten Am</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="member in team.members">
<TableCell>{{ member.name }}</TableCell>
<TableCell>{{ member.email }}</TableCell>
<TableCell>{{ member.has_multi_factor ? 'Ja' : 'Nein' }}</TableCell>
<TableCell>{{ member.role }}</TableCell>
<TableCell>{{ member.joined_at }}</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
</Card>
</div>
</template>
+3 -2
View File
@@ -3,14 +3,15 @@ export interface Team {
slug: string; slug: string;
members: TeamMember[]; members: TeamMember[];
multi_factor_required: boolean; multi_factor_required: boolean;
created_at: string
} }
export type TeamMemberRole = 'admin' | 'member' export type TeamMemberRole = 'admin' | 'member';
export interface TeamMember { export interface TeamMember {
name: string; name: string;
email: string; email: string;
has_multi_factor: boolean; has_multi_factor: boolean;
role: TeamMemberRole; role: TeamMemberRole;
joined_at: string
} }
+5 -1
View File
@@ -10,7 +10,11 @@ Route::middleware(['auth'])->group(function () {
Route::controller(TeamController::class)->prefix('teams')->name('teams.')->group(function () { Route::controller(TeamController::class)->prefix('teams')->name('teams.')->group(function () {
Route::get('/', 'index')->name('index'); Route::get('/', 'index')->name('index');
Route::get('/create', 'create')->name('create'); Route::middleware('verified')->get('/create', 'create')->name('create');
Route::middleware('verified')->post('/', 'store')->name('store');
Route::get('/{team:slug}', 'show')->name('show');
Route::get('/edit/{team:slug}', 'edit')->name('edit');
Route::patch('/{team:slug}', 'patch')->name('patch');
}); });
}); });
+8 -1
View File
@@ -61,16 +61,23 @@ export default defineConfig({
}, },
fmt: { fmt: {
printWidth: 80, printWidth: 80,
tabWidth: 4, tabWidth: 2,
singleQuote: true, singleQuote: true,
semi: true, semi: true,
singleAttributePerLine: false, singleAttributePerLine: false,
htmlWhitespaceSensitivity: 'css', htmlWhitespaceSensitivity: 'css',
ignorePatterns: [ ignorePatterns: [
'**/.json',
'**/*.md', '**/*.md',
'.github/**', '.github/**',
'html/**', 'html/**',
'boost.json', 'boost.json',
'pint.json',
'components.json',
'package.json',
'tsconfig.json',
'vite.config.ts',
'pnpm-workspace.yaml',
'composer.json', 'composer.json',
'docker-compose.yml', 'docker-compose.yml',
'docker/**', 'docker/**',