formatted, added team index, create, show
This commit is contained in:
@@ -2,11 +2,13 @@
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Enums\TeamMemberRole;
|
||||
use App\Http\Requests\StoreTeamRequest;
|
||||
use App\Http\Requests\UpdateTeamRequest;
|
||||
use App\Http\Resources\TeamResource;
|
||||
use App\Models\Team;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Inertia\Inertia;
|
||||
|
||||
class TeamController extends Controller
|
||||
@@ -17,13 +19,17 @@ class TeamController extends Controller
|
||||
public function index(Request $request)
|
||||
{
|
||||
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)
|
||||
{
|
||||
//
|
||||
$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.
|
||||
|
||||
@@ -28,4 +28,14 @@ class StoreTeamRequest extends FormRequest
|
||||
'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,
|
||||
'has_multi_factor' => $this->hasVerifiedEmail() || $this->hasEnabledTwoFactorAuthentication(),
|
||||
'role' => $this->pivot->role,
|
||||
'joined_at' => $this->pivot->created_at->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,8 +21,9 @@ class TeamResource extends JsonResource
|
||||
return [
|
||||
'name' => $this->name,
|
||||
'slug' => $this->slug,
|
||||
'members' => TeamMemberResource::collection($this->members),
|
||||
'members' => TeamMemberResource::collection($this->members)->resolve($request),
|
||||
'multi_factor_required' => $this->require_2fa,
|
||||
'created_at' => $this->created_at->toDateTimeString(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://shadcn-vue.com/schema.json",
|
||||
"style": "new-york-v4",
|
||||
"style": "new-york",
|
||||
"tailwind": {
|
||||
"config": "tailwind.config.js",
|
||||
"css": "resources/css/app.css",
|
||||
|
||||
@@ -20,7 +20,6 @@
|
||||
"require-dev": {
|
||||
"barryvdh/laravel-ide-helper": "^3.7",
|
||||
"fakerphp/faker": "^1.24",
|
||||
"larastan/larastan": "^3.9",
|
||||
"laravel/boost": "^2.2",
|
||||
"laravel/pail": "^1.2.5",
|
||||
"laravel/pao": "^1.0.6",
|
||||
@@ -69,13 +68,9 @@
|
||||
"pnpm run types:check",
|
||||
"@test"
|
||||
],
|
||||
"types:check": [
|
||||
"phpstan analyse --memory-limit=512M"
|
||||
],
|
||||
"test": [
|
||||
"@php artisan config:clear --ansi",
|
||||
"@lint:check",
|
||||
"@types:check",
|
||||
"@php artisan test"
|
||||
],
|
||||
"post-autoload-dump": [
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@
|
||||
"build:ssr": "vp build && vp build --ssr",
|
||||
"dev": "vp dev",
|
||||
"check": "vp check",
|
||||
"check:fix": "vp check --fix",
|
||||
"format": "vp check --fix",
|
||||
"types:check": "vue-tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -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
|
||||
@@ -9,9 +9,8 @@
|
||||
|
||||
@theme inline {
|
||||
--font-sans:
|
||||
Instrument Sans, ui-sans-serif, system-ui, sans-serif,
|
||||
'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||
'Noto Color Emoji';
|
||||
Instrument Sans, ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji',
|
||||
'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
|
||||
@@ -83,18 +83,12 @@ const rightNavItems: NavItem[] = [
|
||||
<div class="lg:hidden">
|
||||
<Sheet>
|
||||
<SheetTrigger :as-child="true">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="mr-2 h-9 w-9"
|
||||
>
|
||||
<Button variant="ghost" size="icon" class="mr-2 h-9 w-9">
|
||||
<Menu class="h-5 w-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" class="w-[300px] p-6">
|
||||
<SheetTitle class="sr-only"
|
||||
>Navigation menu</SheetTitle
|
||||
>
|
||||
<SheetTitle class="sr-only">Navigation menu</SheetTitle>
|
||||
<SheetHeader class="flex justify-start text-left">
|
||||
<AppLogoIcon
|
||||
class="size-6 fill-current text-black dark:text-white"
|
||||
@@ -109,12 +103,7 @@ const rightNavItems: NavItem[] = [
|
||||
:key="item.title"
|
||||
:href="item.href"
|
||||
class="hover:bg-accent flex items-center gap-x-3 rounded-lg px-3 py-2 text-sm font-medium"
|
||||
:class="
|
||||
whenCurrentUrl(
|
||||
item.href,
|
||||
activeItemStyles,
|
||||
)
|
||||
"
|
||||
:class="whenCurrentUrl(item.href, activeItemStyles)"
|
||||
>
|
||||
<component
|
||||
v-if="item.icon"
|
||||
@@ -153,9 +142,7 @@ const rightNavItems: NavItem[] = [
|
||||
<!-- Desktop Menu -->
|
||||
<div class="hidden h-full lg:flex lg:flex-1">
|
||||
<NavigationMenu class="ml-10 flex h-full items-stretch">
|
||||
<NavigationMenuList
|
||||
class="flex h-full items-stretch space-x-2"
|
||||
>
|
||||
<NavigationMenuList class="flex h-full items-stretch space-x-2">
|
||||
<NavigationMenuItem
|
||||
v-for="(item, index) in mainNavItems"
|
||||
:key="index"
|
||||
@@ -164,10 +151,7 @@ const rightNavItems: NavItem[] = [
|
||||
<Link
|
||||
:class="[
|
||||
navigationMenuTriggerStyle(),
|
||||
whenCurrentUrl(
|
||||
item.href,
|
||||
activeItemStyles,
|
||||
),
|
||||
whenCurrentUrl(item.href, activeItemStyles),
|
||||
'h-9 cursor-pointer px-3',
|
||||
]"
|
||||
:href="item.href"
|
||||
@@ -195,16 +179,11 @@ const rightNavItems: NavItem[] = [
|
||||
size="icon"
|
||||
class="group h-9 w-9 cursor-pointer"
|
||||
>
|
||||
<Search
|
||||
class="size-5 opacity-80 group-hover:opacity-100"
|
||||
/>
|
||||
<Search class="size-5 opacity-80 group-hover:opacity-100" />
|
||||
</Button>
|
||||
|
||||
<div class="hidden space-x-1 lg:flex">
|
||||
<template
|
||||
v-for="item in rightNavItems"
|
||||
:key="item.title"
|
||||
>
|
||||
<template v-for="item in rightNavItems" :key="item.title">
|
||||
<TooltipProvider :delay-duration="0">
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
@@ -219,9 +198,7 @@ const rightNavItems: NavItem[] = [
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span class="sr-only">{{
|
||||
item.title
|
||||
}}</span>
|
||||
<span class="sr-only">{{ item.title }}</span>
|
||||
<component
|
||||
:is="item.icon"
|
||||
class="size-5 opacity-80 group-hover:opacity-100"
|
||||
@@ -245,9 +222,7 @@ const rightNavItems: NavItem[] = [
|
||||
size="icon"
|
||||
class="focus-within:ring-primary relative size-10 w-auto rounded-full p-1 focus-within:ring-2"
|
||||
>
|
||||
<Avatar
|
||||
class="size-8 overflow-hidden rounded-full"
|
||||
>
|
||||
<Avatar class="size-8 overflow-hidden rounded-full">
|
||||
<AvatarImage
|
||||
v-if="auth.user.avatar"
|
||||
:src="auth.user.avatar"
|
||||
|
||||
@@ -12,8 +12,6 @@ const name = usePage().props.name;
|
||||
<AppLogoIcon class="size-5 fill-current text-white dark:text-black" />
|
||||
</div>
|
||||
<div class="ml-1 grid flex-1 text-left text-sm">
|
||||
<span class="mb-0.5 truncate leading-tight font-semibold">{{
|
||||
name
|
||||
}}</span>
|
||||
<span class="mb-0.5 truncate leading-tight font-semibold">{{ name }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
} from '@/components/ui/sidebar';
|
||||
import { dashboard } from '@/routes';
|
||||
import type { NavItem } from '@/types';
|
||||
import teams from "@/routes/teams";
|
||||
import teams from '@/routes/teams';
|
||||
|
||||
const mainNavItems: NavItem[] = [
|
||||
{
|
||||
@@ -26,8 +26,7 @@ const mainNavItems: NavItem[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const footerNavItems: NavItem[] = [
|
||||
];
|
||||
const footerNavItems: NavItem[] = [];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -56,22 +56,17 @@ const passwordInput = useTemplateRef('passwordInput');
|
||||
>
|
||||
<DialogHeader class="space-y-3">
|
||||
<DialogTitle
|
||||
>Are you sure you want to delete your
|
||||
account?</DialogTitle
|
||||
>Are you sure you want to delete your account?</DialogTitle
|
||||
>
|
||||
<DialogDescription>
|
||||
Once your account is deleted, all of its
|
||||
resources and data will also be permanently
|
||||
deleted. Please enter your password to confirm
|
||||
you would like to permanently delete your
|
||||
account.
|
||||
Once your account is deleted, all of its resources and data will
|
||||
also be permanently deleted. Please enter your password to
|
||||
confirm you would like to permanently delete your account.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="grid gap-2">
|
||||
<Label for="password" class="sr-only"
|
||||
>Password</Label
|
||||
>
|
||||
<Label for="password" class="sr-only">Password</Label>
|
||||
<PasswordInput
|
||||
id="password"
|
||||
name="password"
|
||||
|
||||
@@ -40,9 +40,9 @@ onUnmounted(() => clearTwoFactorAuthData());
|
||||
class="flex flex-col items-start justify-start space-y-4"
|
||||
>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
When you enable two-factor authentication, you will be prompted
|
||||
for a secure pin during login. This pin can be retrieved from a
|
||||
TOTP-supported application on your phone.
|
||||
When you enable two-factor authentication, you will be prompted for a
|
||||
secure pin during login. This pin can be retrieved from a TOTP-supported
|
||||
application on your phone.
|
||||
</p>
|
||||
|
||||
<div>
|
||||
@@ -55,27 +55,20 @@ onUnmounted(() => clearTwoFactorAuthData());
|
||||
@success="showSetupModal = true"
|
||||
#default="{ processing }"
|
||||
>
|
||||
<Button type="submit" :disabled="processing">
|
||||
Enable 2FA
|
||||
</Button>
|
||||
<Button type="submit" :disabled="processing"> Enable 2FA </Button>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col items-start justify-start space-y-4">
|
||||
<p class="text-muted-foreground text-sm">
|
||||
You will be prompted for a secure, random pin during login,
|
||||
which you can retrieve from the TOTP-supported application on
|
||||
your phone.
|
||||
You will be prompted for a secure, random pin during login, which you
|
||||
can retrieve from the TOTP-supported application on your phone.
|
||||
</p>
|
||||
|
||||
<div class="relative inline">
|
||||
<Form v-bind="disable.form()" #default="{ processing }">
|
||||
<Button
|
||||
variant="destructive"
|
||||
type="submit"
|
||||
:disabled="processing"
|
||||
>
|
||||
<Button variant="destructive" type="submit" :disabled="processing">
|
||||
Disable 2FA
|
||||
</Button>
|
||||
</Form>
|
||||
|
||||
@@ -38,11 +38,7 @@ const { isMobile, state } = useSidebar();
|
||||
<DropdownMenuContent
|
||||
class="w-(--reka-dropdown-menu-trigger-width) min-w-56 rounded-lg"
|
||||
:side="
|
||||
isMobile
|
||||
? 'bottom'
|
||||
: state === 'collapsed'
|
||||
? 'left'
|
||||
: 'bottom'
|
||||
isMobile ? 'bottom' : state === 'collapsed' ? 'left' : 'bottom'
|
||||
"
|
||||
align="end"
|
||||
:side-offset="4"
|
||||
|
||||
@@ -74,8 +74,8 @@ const handleDelete = () => {
|
||||
<DialogContent>
|
||||
<DialogTitle>Remove passkey</DialogTitle>
|
||||
<DialogDescription>
|
||||
Are you sure you want to remove the "{{ passkey.name }}"
|
||||
passkey? You will no longer be able to use it to sign in.
|
||||
Are you sure you want to remove the "{{ passkey.name }}" passkey? You
|
||||
will no longer be able to use it to sign in.
|
||||
</DialogDescription>
|
||||
<DialogFooter class="gap-2">
|
||||
<DialogClose as-child>
|
||||
|
||||
@@ -45,8 +45,8 @@ onMounted(async () => {
|
||||
<LockKeyhole class="size-4" />2FA recovery codes
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
Recovery codes let you regain access if you lose your 2FA
|
||||
device. Store them in a secure password manager.
|
||||
Recovery codes let you regain access if you lose your 2FA device. Store
|
||||
them in a secure password manager.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
@@ -58,8 +58,7 @@ onMounted(async () => {
|
||||
:is="isRecoveryCodesVisible ? EyeOff : Eye"
|
||||
class="size-4"
|
||||
/>
|
||||
{{ isRecoveryCodesVisible ? 'Hide' : 'View' }} recovery
|
||||
codes
|
||||
{{ isRecoveryCodesVisible ? 'Hide' : 'View' }} recovery codes
|
||||
</Button>
|
||||
|
||||
<Form
|
||||
@@ -70,11 +69,7 @@ onMounted(async () => {
|
||||
@success="fetchRecoveryCodes"
|
||||
#default="{ processing }"
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
:disabled="processing"
|
||||
>
|
||||
<Button variant="secondary" type="submit" :disabled="processing">
|
||||
<RefreshCw /> Regenerate codes
|
||||
</Button>
|
||||
</Form>
|
||||
@@ -82,9 +77,7 @@ onMounted(async () => {
|
||||
<div
|
||||
:class="[
|
||||
'relative overflow-hidden transition-all duration-300',
|
||||
isRecoveryCodesVisible
|
||||
? 'h-auto opacity-100'
|
||||
: 'h-0 opacity-0',
|
||||
isRecoveryCodesVisible ? 'h-auto opacity-100' : 'h-0 opacity-0',
|
||||
]"
|
||||
>
|
||||
<div v-if="errors?.length" class="mt-6">
|
||||
@@ -102,18 +95,13 @@ onMounted(async () => {
|
||||
class="bg-muted-foreground/20 h-4 animate-pulse rounded"
|
||||
></div>
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
v-for="(code, index) in recoveryCodesList"
|
||||
:key="index"
|
||||
>
|
||||
<div v-else v-for="(code, index) in recoveryCodesList" :key="index">
|
||||
{{ code }}
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-muted-foreground text-xs select-none">
|
||||
Each recovery code can be used once to access your
|
||||
account and will be removed after use. If you need more,
|
||||
click
|
||||
Each recovery code can be used once to access your account and will
|
||||
be removed after use. If you need more, click
|
||||
<span class="font-bold">Regenerate codes</span> above.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -119,27 +119,21 @@ watch(
|
||||
<div
|
||||
class="border-border bg-muted relative overflow-hidden rounded-full border p-2.5"
|
||||
>
|
||||
<div
|
||||
class="absolute inset-0 grid grid-cols-5 opacity-50"
|
||||
>
|
||||
<div class="absolute inset-0 grid grid-cols-5 opacity-50">
|
||||
<div
|
||||
v-for="i in 5"
|
||||
:key="`col-${i}`"
|
||||
class="border-border border-r last:border-r-0"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="absolute inset-0 grid grid-rows-5 opacity-50"
|
||||
>
|
||||
<div class="absolute inset-0 grid grid-rows-5 opacity-50">
|
||||
<div
|
||||
v-for="i in 5"
|
||||
:key="`row-${i}`"
|
||||
class="border-border border-b last:border-b-0"
|
||||
/>
|
||||
</div>
|
||||
<ScanLine
|
||||
class="text-foreground relative z-20 size-6"
|
||||
/>
|
||||
<ScanLine class="text-foreground relative z-20 size-6" />
|
||||
</div>
|
||||
</div>
|
||||
<DialogTitle>{{ modalConfig.title }}</DialogTitle>
|
||||
@@ -166,10 +160,7 @@ watch(
|
||||
>
|
||||
<Spinner class="size-6" />
|
||||
</div>
|
||||
<div
|
||||
v-else
|
||||
class="relative z-10 overflow-hidden border p-5"
|
||||
>
|
||||
<div v-else class="relative z-10 overflow-hidden border p-5">
|
||||
<div
|
||||
v-html="qrCodeSvg"
|
||||
class="flex aspect-square size-full items-center justify-center"
|
||||
@@ -190,20 +181,14 @@ watch(
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
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="relative flex w-full items-center justify-center">
|
||||
<div class="bg-border absolute inset-0 top-1/2 h-px w-full" />
|
||||
<span class="bg-card relative px-2 py-1"
|
||||
>or, enter the code manually</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="flex w-full items-center justify-center space-x-2"
|
||||
>
|
||||
<div class="flex w-full items-center justify-center space-x-2">
|
||||
<div
|
||||
class="border-border flex w-full items-stretch overflow-hidden rounded-xl border"
|
||||
>
|
||||
@@ -224,10 +209,7 @@ watch(
|
||||
@click="copy(manualSetupKey || '')"
|
||||
class="border-border hover:bg-muted relative block h-auto border-l px-3"
|
||||
>
|
||||
<Check
|
||||
v-if="copied"
|
||||
class="w-4 text-green-500"
|
||||
/>
|
||||
<Check v-if="copied" class="w-4 text-green-500" />
|
||||
<Copy v-else class="w-4" />
|
||||
</button>
|
||||
</template>
|
||||
@@ -246,10 +228,7 @@ watch(
|
||||
v-slot="{ errors, processing }"
|
||||
>
|
||||
<input type="hidden" name="code" :value="code" />
|
||||
<div
|
||||
ref="pinInputContainerRef"
|
||||
class="relative w-full space-y-3"
|
||||
>
|
||||
<div ref="pinInputContainerRef" class="relative w-full space-y-3">
|
||||
<div
|
||||
class="flex w-full flex-col items-center justify-center space-y-3 py-2"
|
||||
>
|
||||
|
||||
@@ -16,15 +16,10 @@ export function updateTheme(value: Appearance): void {
|
||||
}
|
||||
|
||||
if (value === 'system') {
|
||||
const mediaQueryList = window.matchMedia(
|
||||
'(prefers-color-scheme: dark)',
|
||||
);
|
||||
const mediaQueryList = window.matchMedia('(prefers-color-scheme: dark)');
|
||||
const systemTheme = mediaQueryList.matches ? 'dark' : 'light';
|
||||
|
||||
document.documentElement.classList.toggle(
|
||||
'dark',
|
||||
systemTheme === 'dark',
|
||||
);
|
||||
document.documentElement.classList.toggle('dark', systemTheme === 'dark');
|
||||
} else {
|
||||
document.documentElement.classList.toggle('dark', value === 'dark');
|
||||
}
|
||||
|
||||
@@ -26,9 +26,7 @@ defineProps<{
|
||||
class="flex items-center gap-2 self-center font-medium"
|
||||
>
|
||||
<div class="flex h-9 w-9 items-center justify-center">
|
||||
<AppLogoIcon
|
||||
class="size-9 fill-current text-black dark:text-white"
|
||||
/>
|
||||
<AppLogoIcon class="size-9 fill-current text-black dark:text-white" />
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
|
||||
@@ -37,10 +37,7 @@ const { isCurrentOrParentUrl } = useCurrentUrl();
|
||||
|
||||
<div class="flex flex-col lg:flex-row lg:space-x-12">
|
||||
<aside class="w-full max-w-xl lg:w-48">
|
||||
<nav
|
||||
class="flex flex-col space-y-1 space-x-0"
|
||||
aria-label="Settings"
|
||||
>
|
||||
<nav class="flex flex-col space-y-1 space-x-0" aria-label="Settings">
|
||||
<Button
|
||||
v-for="item in sidebarNavItems"
|
||||
:key="toUrl(item.href)"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { InertiaLinkProps } from '@inertiajs/vue3';
|
||||
import { clsx } from 'clsx';
|
||||
import type { ClassValue } from 'clsx';
|
||||
import { clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
|
||||
@@ -18,9 +18,7 @@ defineOptions({
|
||||
<template>
|
||||
<Head title="Dashboard" />
|
||||
|
||||
<div
|
||||
class="flex h-full flex-1 flex-col gap-4 overflow-x-auto rounded-xl p-4"
|
||||
>
|
||||
<div 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="border-sidebar-border/70 dark:border-sidebar-border relative aspect-video overflow-hidden rounded-xl border"
|
||||
|
||||
@@ -50,16 +50,14 @@ import { register } from '@/routes';
|
||||
>
|
||||
<h1 class="mb-1 font-medium">Let's get started</h1>
|
||||
<p class="mb-2 text-[#706f6c] dark:text-[#A1A09A]">
|
||||
Laravel has an incredibly rich ecosystem. <br />We
|
||||
suggest starting with the following.
|
||||
Laravel has an incredibly rich ecosystem. <br />We suggest starting
|
||||
with the following.
|
||||
</p>
|
||||
<ul class="mb-4 flex flex-col lg:mb-6">
|
||||
<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]"
|
||||
>
|
||||
<span
|
||||
class="relative bg-white py-1 dark:bg-[#161615]"
|
||||
>
|
||||
<span class="relative bg-white py-1 dark:bg-[#161615]">
|
||||
<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]"
|
||||
>
|
||||
@@ -96,9 +94,7 @@ import { register } from '@/routes';
|
||||
<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]"
|
||||
>
|
||||
<span
|
||||
class="relative bg-white py-1 dark:bg-[#161615]"
|
||||
>
|
||||
<span class="relative bg-white py-1 dark:bg-[#161615]">
|
||||
<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]"
|
||||
>
|
||||
@@ -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"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M438 -3H421.694V102.197H438V-3Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path d="M438 -3H421.694V102.197H438V-3Z" fill="currentColor" />
|
||||
<path
|
||||
d="M139.43 102.197H155.735V48.2834H183.712V32.1665H139.43V102.197Z"
|
||||
fill="currentColor"
|
||||
@@ -300,19 +293,11 @@ import { register } from '@/routes';
|
||||
height="299"
|
||||
fill="black"
|
||||
>
|
||||
<rect
|
||||
fill="white"
|
||||
x="51"
|
||||
y="103"
|
||||
width="338"
|
||||
height="299"
|
||||
/>
|
||||
<rect fill="white" x="51" y="103" width="338" height="299" />
|
||||
<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"
|
||||
/>
|
||||
<path
|
||||
d="M78.2 167.6H53V105.2H146V400.2H78.2V167.6Z"
|
||||
/>
|
||||
<path d="M78.2 167.6H53V105.2H146V400.2H78.2V167.6Z" />
|
||||
</mask>
|
||||
<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"
|
||||
|
||||
@@ -34,11 +34,7 @@ defineOptions({
|
||||
separator="Or confirm with password"
|
||||
/>
|
||||
|
||||
<Form
|
||||
v-bind="store.form()"
|
||||
reset-on-success
|
||||
v-slot="{ errors, processing }"
|
||||
>
|
||||
<Form v-bind="store.form()" reset-on-success v-slot="{ errors, processing }">
|
||||
<div class="space-y-6">
|
||||
<div class="grid gap-2">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
|
||||
@@ -97,10 +97,7 @@ defineOptions({
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-4">
|
||||
<Button
|
||||
:disabled="processing"
|
||||
data-test="update-password-button"
|
||||
>
|
||||
<Button :disabled="processing" data-test="update-password-button">
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
@@ -113,8 +110,5 @@ defineOptions({
|
||||
:twoFactorEnabled="twoFactorEnabled"
|
||||
/>
|
||||
|
||||
<ManagePasskeys
|
||||
:canManagePasskeys="canManagePasskeys"
|
||||
:passkeys="passkeys"
|
||||
/>
|
||||
<ManagePasskeys :canManagePasskeys="canManagePasskeys" :passkeys="passkeys" />
|
||||
</template>
|
||||
|
||||
@@ -1,11 +1,66 @@
|
||||
<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>
|
||||
|
||||
<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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
<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>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
||||
@@ -1,25 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
|
||||
import teamRoutes from "@/routes/teams";
|
||||
import {Head} from "@inertiajs/vue3";
|
||||
import type {Team} from "@/types/models";
|
||||
import TextLink from "@/components/TextLink.vue";
|
||||
import {Card, CardContent} from "@/components/ui/card";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCaption,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table"
|
||||
<script lang="ts" setup>
|
||||
import teamRoutes from '@/routes/teams';
|
||||
import {Head, Link, router, usePage} from '@inertiajs/vue3';
|
||||
import type {Team} from '@/types/models';
|
||||
import TextLink from '@/components/TextLink.vue';
|
||||
import {Card, CardContent} from '@/components/ui/card';
|
||||
import {Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow,} from '@/components/ui/table';
|
||||
import {Button} from '@/components/ui/button';
|
||||
import {Eye, Pencil, Plus} from '@lucide/vue';
|
||||
|
||||
defineOptions({
|
||||
layout: {
|
||||
breadcrumbs: [
|
||||
{
|
||||
title: 'Meine Teams',
|
||||
title: 'Teams',
|
||||
href: teamRoutes.index(),
|
||||
},
|
||||
],
|
||||
@@ -27,14 +20,31 @@ defineOptions({
|
||||
});
|
||||
|
||||
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>
|
||||
|
||||
<template>
|
||||
<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">
|
||||
<CardContent>
|
||||
@@ -49,19 +59,31 @@ defineProps<{
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow
|
||||
v-if="teams.length > 0"
|
||||
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.slug }}</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>
|
||||
</TableBody>
|
||||
<TableCaption v-if="teams.length <= 0">
|
||||
Du bist noch von keinem Team mitglied.
|
||||
Tritt einem Team bei oder
|
||||
<TextLink href="{{ route('teams.create') }}">erstelle eins.</TextLink>
|
||||
Du bist noch von keinem Team mitglied. Tritt einem Team bei oder
|
||||
<TextLink :href="teamRoutes.create()">erstelle eins.</TextLink>
|
||||
</TableCaption>
|
||||
</Table>
|
||||
</CardContent>
|
||||
|
||||
@@ -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,14 +3,15 @@ export interface Team {
|
||||
slug: string;
|
||||
members: TeamMember[];
|
||||
multi_factor_required: boolean;
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export type TeamMemberRole = 'admin' | 'member'
|
||||
export type TeamMemberRole = 'admin' | 'member';
|
||||
|
||||
export interface TeamMember {
|
||||
name: string;
|
||||
email: string;
|
||||
has_multi_factor: boolean;
|
||||
role: TeamMemberRole;
|
||||
joined_at: string
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -10,7 +10,11 @@ Route::middleware(['auth'])->group(function () {
|
||||
|
||||
Route::controller(TeamController::class)->prefix('teams')->name('teams.')->group(function () {
|
||||
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
@@ -61,16 +61,23 @@ export default defineConfig({
|
||||
},
|
||||
fmt: {
|
||||
printWidth: 80,
|
||||
tabWidth: 4,
|
||||
tabWidth: 2,
|
||||
singleQuote: true,
|
||||
semi: true,
|
||||
singleAttributePerLine: false,
|
||||
htmlWhitespaceSensitivity: 'css',
|
||||
ignorePatterns: [
|
||||
'**/.json',
|
||||
'**/*.md',
|
||||
'.github/**',
|
||||
'html/**',
|
||||
'boost.json',
|
||||
'pint.json',
|
||||
'components.json',
|
||||
'package.json',
|
||||
'tsconfig.json',
|
||||
'vite.config.ts',
|
||||
'pnpm-workspace.yaml',
|
||||
'composer.json',
|
||||
'docker-compose.yml',
|
||||
'docker/**',
|
||||
|
||||
Reference in New Issue
Block a user