add linter, prettier and typescript support for jetstream (#1)

* add linter, prettier and typescript support for jetstream

* add github actions for lint, build and typecheck

* add composer install to build action

* fix composer lock

* add ext-intl and fixed php version

* fix intl php extension install

* add tip php extension to github npm build action

* fix php version to 8.3.1

* change github php base action

* fix primary button default type

* updated typescript types from Team to Organization

---------

Co-authored-by: Gregor Vostrak <gregorvostrak@Gregors-MacBook-Pro.local>
This commit is contained in:
Gregor Vostrak
2024-01-21 22:35:43 +01:00
committed by GitHub
parent 0a8d958ccc
commit 27140d4ffc
73 changed files with 4727 additions and 886 deletions

13
.eslintrc.cjs Normal file
View File

@@ -0,0 +1,13 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")
module.exports = {
extends: [
'plugin:vue/vue3-essential',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier'
],
rules: {
'vue/multi-word-component-names': 'off',
}
}

22
.github/workflows/npm-build.yml vendored Normal file
View File

@@ -0,0 +1,22 @@
name: NPM Build
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3.1'
extensions: intl, zip
- run: composer install
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '20.x'
- run: npm ci
- run: npm run build

17
.github/workflows/npm-lint.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: NPM Lint
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '20.x'
- run: npm ci
- run: npm run lint

17
.github/workflows/npm-typecheck.yml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: NPM Typecheck
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: '20.x'
- run: npm ci
- run: npm run type-check

7
.prettierrc.json Normal file
View File

@@ -0,0 +1,7 @@
{
"trailingComma": "es5",
"tabWidth": 4,
"singleQuote": true,
"bracketSameLine": true,
"quoteProps": "preserve"
}

View File

@@ -20,6 +20,7 @@
"require-dev": {
"brianium/paratest": "^7.3",
"fakerphp/faker": "^1.9.1",
"fumeapp/modeltyper": "^2.2",
"larastan/larastan": "^2.0",
"laravel/pint": "^1.0",
"laravel/sail": "^1.18",
@@ -58,6 +59,9 @@
"analyse": [
"@php ./vendor/bin/phpstan analyse --memory-limit=2G --configuration=phpstan.neon"
],
"generate-typescript": [
"@php artisan model:typer > ./resources/js/types/models.ts"
],
"ptest": [
"@php artisan test --parallel --colors=always --stop-on-failure"
],

106
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "68ea9179372fd4db583f50784a1ca6f7",
"content-hash": "ad3e9deab517156569b424191cdb7871",
"packages": [
{
"name": "anourvalar/eloquent-serialize",
@@ -792,16 +792,16 @@
},
{
"name": "doctrine/dbal",
"version": "3.7.2",
"version": "3.7.3",
"source": {
"type": "git",
"url": "https://github.com/doctrine/dbal.git",
"reference": "0ac3c270590e54910715e9a1a044cc368df282b2"
"reference": "ce594cbc39a4866c544f1a970d285ff0548221ad"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/0ac3c270590e54910715e9a1a044cc368df282b2",
"reference": "0ac3c270590e54910715e9a1a044cc368df282b2",
"url": "https://api.github.com/repos/doctrine/dbal/zipball/ce594cbc39a4866c544f1a970d285ff0548221ad",
"reference": "ce594cbc39a4866c544f1a970d285ff0548221ad",
"shasum": ""
},
"require": {
@@ -817,14 +817,14 @@
"doctrine/coding-standard": "12.0.0",
"fig/log-test": "^1",
"jetbrains/phpstorm-stubs": "2023.1",
"phpstan/phpstan": "1.10.42",
"phpstan/phpstan": "1.10.56",
"phpstan/phpstan-strict-rules": "^1.5",
"phpunit/phpunit": "9.6.13",
"phpunit/phpunit": "9.6.15",
"psalm/plugin-phpunit": "0.18.4",
"slevomat/coding-standard": "8.13.1",
"squizlabs/php_codesniffer": "3.7.2",
"symfony/cache": "^5.4|^6.0",
"symfony/console": "^4.4|^5.4|^6.0",
"squizlabs/php_codesniffer": "3.8.1",
"symfony/cache": "^5.4|^6.0|^7.0",
"symfony/console": "^4.4|^5.4|^6.0|^7.0",
"vimeo/psalm": "4.30.0"
},
"suggest": {
@@ -885,7 +885,7 @@
],
"support": {
"issues": "https://github.com/doctrine/dbal/issues",
"source": "https://github.com/doctrine/dbal/tree/3.7.2"
"source": "https://github.com/doctrine/dbal/tree/3.7.3"
},
"funding": [
{
@@ -901,7 +901,7 @@
"type": "tidelift"
}
],
"time": "2023-11-19T08:06:58+00:00"
"time": "2024-01-21T07:53:09+00:00"
},
{
"name": "doctrine/deprecations",
@@ -4522,31 +4522,31 @@
},
{
"name": "nette/schema",
"version": "v1.2.5",
"version": "v1.3.0",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
"reference": "0462f0166e823aad657c9224d0f849ecac1ba10a"
"reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/0462f0166e823aad657c9224d0f849ecac1ba10a",
"reference": "0462f0166e823aad657c9224d0f849ecac1ba10a",
"url": "https://api.github.com/repos/nette/schema/zipball/a6d3a6d1f545f01ef38e60f375d1cf1f4de98188",
"reference": "a6d3a6d1f545f01ef38e60f375d1cf1f4de98188",
"shasum": ""
},
"require": {
"nette/utils": "^2.5.7 || ^3.1.5 || ^4.0",
"php": "7.1 - 8.3"
"nette/utils": "^4.0",
"php": "8.1 - 8.3"
},
"require-dev": {
"nette/tester": "^2.3 || ^2.4",
"nette/tester": "^2.4",
"phpstan/phpstan-nette": "^1.0",
"tracy/tracy": "^2.7"
"tracy/tracy": "^2.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.2-dev"
"dev-master": "1.3-dev"
}
},
"autoload": {
@@ -4578,9 +4578,9 @@
],
"support": {
"issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.2.5"
"source": "https://github.com/nette/schema/tree/v1.3.0"
},
"time": "2023-10-05T20:37:59+00:00"
"time": "2023-12-11T11:54:22+00:00"
},
{
"name": "nette/utils",
@@ -9466,6 +9466,66 @@
],
"time": "2023-11-03T12:00:00+00:00"
},
{
"name": "fumeapp/modeltyper",
"version": "v2.2.7",
"source": {
"type": "git",
"url": "https://github.com/fumeapp/modeltyper.git",
"reference": "eeae47953b74aa74a67c7c7d6235418f02a31429"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/fumeapp/modeltyper/zipball/eeae47953b74aa74a67c7c7d6235418f02a31429",
"reference": "eeae47953b74aa74a67c7c7d6235418f02a31429",
"shasum": ""
},
"require": {
"illuminate/console": "^10.0.0",
"illuminate/database": "^10.0.0",
"illuminate/support": "^10.0.0",
"php": "^8.1"
},
"require-dev": {
"consolidation/robo": "^4.0",
"doctrine/dbal": "^3.6",
"laravel/pint": "^1.7",
"nunomaduro/larastan": "^2.2",
"orchestra/testbench": "^8.0",
"phpunit/phpunit": "^10.0",
"spatie/laravel-ray": "^1.32",
"totten/lurkerlite": "^1.3"
},
"type": "library",
"extra": {
"laravel": {
"providers": [
"FumeApp\\ModelTyper\\ModelTyperServiceProvider"
]
}
},
"autoload": {
"psr-4": {
"FumeApp\\ModelTyper\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "kevin olson",
"email": "acidjazz@gmail.com"
}
],
"description": "Generate TypeScript interfaces from Laravel Models",
"support": {
"issues": "https://github.com/fumeapp/modeltyper/issues",
"source": "https://github.com/fumeapp/modeltyper/tree/v2.2.7"
},
"time": "2023-12-20T12:55:56+00:00"
},
{
"name": "hamcrest/hamcrest-php",
"version": "v2.0.1",

2156
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -3,19 +3,33 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
"build": "vite build",
"lint": "eslint --ext .js,.vue,.ts --ignore-path .gitignore .",
"lint:fix": "eslint --fix --ext .js,.vue,.ts --ignore-path .gitignore .",
"type-check": "vue-tsc --noEmit"
},
"devDependencies": {
"@inertiajs/vue3": "^1.0.0",
"@tailwindcss/forms": "^0.5.2",
"@tailwindcss/typography": "^0.5.2",
"@types/ziggy-js": "^1.8.0",
"@vitejs/plugin-vue": "^4.5.0",
"@vue/tsconfig": "^0.5.1",
"autoprefixer": "^10.4.7",
"axios": "^1.6.4",
"laravel-vite-plugin": "^1.0.0",
"postcss": "^8.4.14",
"tailwindcss": "^3.1.0",
"typescript": "^5.3.3",
"vite": "^5.0.0",
"vue": "^3.2.31"
"vite-plugin-checker": "^0.6.2",
"vue": "^3.2.31",
"vue-tsc": "^1.8.27",
"ziggy-js": "^1.8.1"
},
"dependencies": {
"@rushstack/eslint-patch": "^1.7.0",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0"
}
}

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
defineProps({
on: Boolean,
});
@@ -6,7 +6,10 @@ defineProps({
<template>
<div>
<transition leave-active-class="transition ease-in duration-1000" leave-from-class="opacity-100" leave-to-class="opacity-0">
<transition
leave-active-class="transition ease-in duration-1000"
leave-from-class="opacity-100"
leave-to-class="opacity-0">
<div v-show="on" class="text-sm text-gray-600 dark:text-gray-400">
<slot />
</div>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import SectionTitle from './SectionTitle.vue';
</script>
@@ -14,7 +14,8 @@ import SectionTitle from './SectionTitle.vue';
</SectionTitle>
<div class="mt-5 md:mt-0 md:col-span-2">
<div class="px-4 py-5 sm:p-6 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<div
class="px-4 py-5 sm:p-6 bg-white dark:bg-gray-800 shadow sm:rounded-lg">
<slot name="content" />
</div>
</div>

View File

@@ -1,7 +1,13 @@
<template>
<svg viewBox="0 0 317 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M74.09 30.04V13h-4.14v21H82.1v-3.96h-8.01zM95.379 19v1.77c-1.08-1.35-2.7-2.19-4.89-2.19-3.99 0-7.29 3.45-7.29 7.92s3.3 7.92 7.29 7.92c2.19 0 3.81-.84 4.89-2.19V34h3.87V19h-3.87zm-4.17 11.73c-2.37 0-4.14-1.71-4.14-4.23 0-2.52 1.77-4.23 4.14-4.23 2.4 0 4.17 1.71 4.17 4.23 0 2.52-1.77 4.23-4.17 4.23zM106.628 21.58V19h-3.87v15h3.87v-7.17c0-3.15 2.55-4.05 4.56-3.81V18.7c-1.89 0-3.78.84-4.56 2.88zM124.295 19v1.77c-1.08-1.35-2.7-2.19-4.89-2.19-3.99 0-7.29 3.45-7.29 7.92s3.3 7.92 7.29 7.92c2.19 0 3.81-.84 4.89-2.19V34h3.87V19h-3.87zm-4.17 11.73c-2.37 0-4.14-1.71-4.14-4.23 0-2.52 1.77-4.23 4.14-4.23 2.4 0 4.17 1.71 4.17 4.23 0 2.52-1.77 4.23-4.17 4.23zM141.544 19l-3.66 10.5-3.63-10.5h-4.26l5.7 15h4.41l5.7-15h-4.26zM150.354 28.09h11.31c.09-.51.15-1.02.15-1.59 0-4.41-3.15-7.92-7.59-7.92-4.71 0-7.92 3.45-7.92 7.92s3.18 7.92 8.22 7.92c2.88 0 5.13-1.17 6.54-3.21l-3.12-1.8c-.66.87-1.86 1.5-3.36 1.5-2.04 0-3.69-.84-4.23-2.82zm-.06-3c.45-1.92 1.86-3.03 3.93-3.03 1.62 0 3.24.87 3.72 3.03h-7.65zM164.516 34h3.87V12.1h-3.87V34zM185.248 34.36c3.69 0 6.9-2.01 6.9-6.3V13h-2.1v15.06c0 3.03-2.07 4.26-4.8 4.26-2.19 0-3.93-.78-4.62-2.61l-1.77 1.05c1.05 2.43 3.57 3.6 6.39 3.6zM203.124 18.64c-4.65 0-7.83 3.45-7.83 7.86 0 4.53 3.24 7.86 7.98 7.86 3.03 0 5.34-1.41 6.6-3.45l-1.74-1.02c-.81 1.44-2.46 2.55-4.83 2.55-3.18 0-5.55-1.89-5.97-4.95h13.17c.03-.3.06-.63.06-.93 0-4.11-2.85-7.92-7.44-7.92zm0 1.92c2.58 0 4.98 1.71 5.4 5.01h-11.19c.39-2.94 2.64-5.01 5.79-5.01zM221.224 20.92V19h-4.32v-4.2l-1.98.6V19h-3.15v1.92h3.15v9.09c0 3.6 2.25 4.59 6.3 3.99v-1.74c-2.91.12-4.32.33-4.32-2.25v-9.09h4.32zM225.176 22.93c0-1.62 1.59-2.37 3.15-2.37 1.44 0 2.97.57 3.6 2.1l1.65-.96c-.87-1.86-2.79-3.06-5.25-3.06-3 0-5.13 1.89-5.13 4.29 0 5.52 8.76 3.39 8.76 7.11 0 1.77-1.68 2.4-3.45 2.4-2.01 0-3.57-.99-4.11-2.52l-1.68.99c.75 1.92 2.79 3.45 5.79 3.45 3.21 0 5.43-1.77 5.43-4.32 0-5.52-8.76-3.39-8.76-7.11zM244.603 20.92V19h-4.32v-4.2l-1.98.6V19h-3.15v1.92h3.15v9.09c0 3.6 2.25 4.59 6.3 3.99v-1.74c-2.91.12-4.32.33-4.32-2.25v-9.09h4.32zM249.883 21.49V19h-1.98v15h1.98v-8.34c0-3.72 2.34-4.98 4.74-4.98v-1.92c-1.92 0-3.69.63-4.74 2.73zM263.358 18.64c-4.65 0-7.83 3.45-7.83 7.86 0 4.53 3.24 7.86 7.98 7.86 3.03 0 5.34-1.41 6.6-3.45l-1.74-1.02c-.81 1.44-2.46 2.55-4.83 2.55-3.18 0-5.55-1.89-5.97-4.95h13.17c.03-.3.06-.63.06-.93 0-4.11-2.85-7.92-7.44-7.92zm0 1.92c2.58 0 4.98 1.71 5.4 5.01h-11.19c.39-2.94 2.64-5.01 5.79-5.01zM286.848 19v2.94c-1.26-2.01-3.39-3.3-6.06-3.3-4.23 0-7.74 3.42-7.74 7.86s3.51 7.86 7.74 7.86c2.67 0 4.8-1.29 6.06-3.3V34h1.98V19h-1.98zm-5.91 13.44c-3.33 0-5.91-2.61-5.91-5.94 0-3.33 2.58-5.94 5.91-5.94s5.91 2.61 5.91 5.94c0 3.33-2.58 5.94-5.91 5.94zM309.01 18.64c-1.92 0-3.75.87-4.86 2.73-.84-1.74-2.46-2.73-4.56-2.73-1.8 0-3.42.72-4.59 2.55V19h-1.98v15H295v-8.31c0-3.72 2.16-5.13 4.32-5.13 2.13 0 3.51 1.41 3.51 4.08V34h1.98v-8.31c0-3.72 1.86-5.13 4.17-5.13 2.13 0 3.66 1.41 3.66 4.08V34h1.98v-9.36c0-3.75-2.31-6-5.61-6z" class="fill-black dark:fill-white" />
<path d="M11.395 44.428C4.557 40.198 0 32.632 0 24 0 10.745 10.745 0 24 0a23.891 23.891 0 0113.997 4.502c-.2 17.907-11.097 33.245-26.602 39.926z" fill="#6875F5" />
<path d="M14.134 45.885A23.914 23.914 0 0024 48c13.255 0 24-10.745 24-24 0-3.516-.756-6.856-2.115-9.866-4.659 15.143-16.608 27.092-31.75 31.751z" fill="#6875F5" />
<path
d="M74.09 30.04V13h-4.14v21H82.1v-3.96h-8.01zM95.379 19v1.77c-1.08-1.35-2.7-2.19-4.89-2.19-3.99 0-7.29 3.45-7.29 7.92s3.3 7.92 7.29 7.92c2.19 0 3.81-.84 4.89-2.19V34h3.87V19h-3.87zm-4.17 11.73c-2.37 0-4.14-1.71-4.14-4.23 0-2.52 1.77-4.23 4.14-4.23 2.4 0 4.17 1.71 4.17 4.23 0 2.52-1.77 4.23-4.17 4.23zM106.628 21.58V19h-3.87v15h3.87v-7.17c0-3.15 2.55-4.05 4.56-3.81V18.7c-1.89 0-3.78.84-4.56 2.88zM124.295 19v1.77c-1.08-1.35-2.7-2.19-4.89-2.19-3.99 0-7.29 3.45-7.29 7.92s3.3 7.92 7.29 7.92c2.19 0 3.81-.84 4.89-2.19V34h3.87V19h-3.87zm-4.17 11.73c-2.37 0-4.14-1.71-4.14-4.23 0-2.52 1.77-4.23 4.14-4.23 2.4 0 4.17 1.71 4.17 4.23 0 2.52-1.77 4.23-4.17 4.23zM141.544 19l-3.66 10.5-3.63-10.5h-4.26l5.7 15h4.41l5.7-15h-4.26zM150.354 28.09h11.31c.09-.51.15-1.02.15-1.59 0-4.41-3.15-7.92-7.59-7.92-4.71 0-7.92 3.45-7.92 7.92s3.18 7.92 8.22 7.92c2.88 0 5.13-1.17 6.54-3.21l-3.12-1.8c-.66.87-1.86 1.5-3.36 1.5-2.04 0-3.69-.84-4.23-2.82zm-.06-3c.45-1.92 1.86-3.03 3.93-3.03 1.62 0 3.24.87 3.72 3.03h-7.65zM164.516 34h3.87V12.1h-3.87V34zM185.248 34.36c3.69 0 6.9-2.01 6.9-6.3V13h-2.1v15.06c0 3.03-2.07 4.26-4.8 4.26-2.19 0-3.93-.78-4.62-2.61l-1.77 1.05c1.05 2.43 3.57 3.6 6.39 3.6zM203.124 18.64c-4.65 0-7.83 3.45-7.83 7.86 0 4.53 3.24 7.86 7.98 7.86 3.03 0 5.34-1.41 6.6-3.45l-1.74-1.02c-.81 1.44-2.46 2.55-4.83 2.55-3.18 0-5.55-1.89-5.97-4.95h13.17c.03-.3.06-.63.06-.93 0-4.11-2.85-7.92-7.44-7.92zm0 1.92c2.58 0 4.98 1.71 5.4 5.01h-11.19c.39-2.94 2.64-5.01 5.79-5.01zM221.224 20.92V19h-4.32v-4.2l-1.98.6V19h-3.15v1.92h3.15v9.09c0 3.6 2.25 4.59 6.3 3.99v-1.74c-2.91.12-4.32.33-4.32-2.25v-9.09h4.32zM225.176 22.93c0-1.62 1.59-2.37 3.15-2.37 1.44 0 2.97.57 3.6 2.1l1.65-.96c-.87-1.86-2.79-3.06-5.25-3.06-3 0-5.13 1.89-5.13 4.29 0 5.52 8.76 3.39 8.76 7.11 0 1.77-1.68 2.4-3.45 2.4-2.01 0-3.57-.99-4.11-2.52l-1.68.99c.75 1.92 2.79 3.45 5.79 3.45 3.21 0 5.43-1.77 5.43-4.32 0-5.52-8.76-3.39-8.76-7.11zM244.603 20.92V19h-4.32v-4.2l-1.98.6V19h-3.15v1.92h3.15v9.09c0 3.6 2.25 4.59 6.3 3.99v-1.74c-2.91.12-4.32.33-4.32-2.25v-9.09h4.32zM249.883 21.49V19h-1.98v15h1.98v-8.34c0-3.72 2.34-4.98 4.74-4.98v-1.92c-1.92 0-3.69.63-4.74 2.73zM263.358 18.64c-4.65 0-7.83 3.45-7.83 7.86 0 4.53 3.24 7.86 7.98 7.86 3.03 0 5.34-1.41 6.6-3.45l-1.74-1.02c-.81 1.44-2.46 2.55-4.83 2.55-3.18 0-5.55-1.89-5.97-4.95h13.17c.03-.3.06-.63.06-.93 0-4.11-2.85-7.92-7.44-7.92zm0 1.92c2.58 0 4.98 1.71 5.4 5.01h-11.19c.39-2.94 2.64-5.01 5.79-5.01zM286.848 19v2.94c-1.26-2.01-3.39-3.3-6.06-3.3-4.23 0-7.74 3.42-7.74 7.86s3.51 7.86 7.74 7.86c2.67 0 4.8-1.29 6.06-3.3V34h1.98V19h-1.98zm-5.91 13.44c-3.33 0-5.91-2.61-5.91-5.94 0-3.33 2.58-5.94 5.91-5.94s5.91 2.61 5.91 5.94c0 3.33-2.58 5.94-5.91 5.94zM309.01 18.64c-1.92 0-3.75.87-4.86 2.73-.84-1.74-2.46-2.73-4.56-2.73-1.8 0-3.42.72-4.59 2.55V19h-1.98v15H295v-8.31c0-3.72 2.16-5.13 4.32-5.13 2.13 0 3.51 1.41 3.51 4.08V34h1.98v-8.31c0-3.72 1.86-5.13 4.17-5.13 2.13 0 3.66 1.41 3.66 4.08V34h1.98v-9.36c0-3.75-2.31-6-5.61-6z"
class="fill-black dark:fill-white" />
<path
d="M11.395 44.428C4.557 40.198 0 32.632 0 24 0 10.745 10.745 0 24 0a23.891 23.891 0 0113.997 4.502c-.2 17.907-11.097 33.245-26.602 39.926z"
fill="#6875F5" />
<path
d="M14.134 45.885A23.914 23.914 0 0024 48c13.255 0 24-10.745 24-24 0-3.516-.756-6.856-2.115-9.866-4.659 15.143-16.608 27.092-31.75 31.751z"
fill="#6875F5" />
</svg>
</template>

View File

@@ -1,6 +1,10 @@
<template>
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.395 44.428C4.557 40.198 0 32.632 0 24 0 10.745 10.745 0 24 0a23.891 23.891 0 0113.997 4.502c-.2 17.907-11.097 33.245-26.602 39.926z" fill="#6875F5" />
<path d="M14.134 45.885A23.914 23.914 0 0024 48c13.255 0 24-10.745 24-24 0-3.516-.756-6.856-2.115-9.866-4.659 15.143-16.608 27.092-31.75 31.751z" fill="#6875F5" />
<path
d="M11.395 44.428C4.557 40.198 0 32.632 0 24 0 10.745 10.745 0 24 0a23.891 23.891 0 0113.997 4.502c-.2 17.907-11.097 33.245-26.602 39.926z"
fill="#6875F5" />
<path
d="M14.134 45.885A23.914 23.914 0 0024 48c13.255 0 24-10.745 24-24 0-3.516-.756-6.856-2.115-9.866-4.659 15.143-16.608 27.092-31.75 31.751z"
fill="#6875F5" />
</svg>
</template>

View File

@@ -1,10 +1,12 @@
<template>
<div class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
<div
class="min-h-screen flex flex-col sm:justify-center items-center pt-6 sm:pt-0 bg-gray-100 dark:bg-gray-900">
<div>
<slot name="logo" />
</div>
<div class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg">
<div
class="w-full sm:max-w-md mt-6 px-6 py-4 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg">
<slot />
</div>
</div>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
</script>
@@ -8,10 +8,13 @@ import { Link } from '@inertiajs/vue3';
class="w-16 h-16"
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M11.395 44.428C4.557 40.198 0 32.632 0 24 0 10.745 10.745 0 24 0a23.891 23.891 0 0113.997 4.502c-.2 17.907-11.097 33.245-26.602 39.926z" fill="#6875F5" />
<path d="M14.134 45.885A23.914 23.914 0 0024 48c13.255 0 24-10.745 24-24 0-3.516-.756-6.856-2.115-9.866-4.659 15.143-16.608 27.092-31.75 31.751z" fill="#6875F5" />
xmlns="http://www.w3.org/2000/svg">
<path
d="M11.395 44.428C4.557 40.198 0 32.632 0 24 0 10.745 10.745 0 24 0a23.891 23.891 0 0113.997 4.502c-.2 17.907-11.097 33.245-26.602 39.926z"
fill="#6875F5" />
<path
d="M14.134 45.885A23.914 23.914 0 0024 48c13.255 0 24-10.745 24-24 0-3.516-.756-6.856-2.115-9.866-4.659 15.143-16.608 27.092-31.75 31.751z"
fill="#6875F5" />
</svg>
</Link>
</template>

View File

@@ -1,8 +1,16 @@
<script setup>
<script setup lang="ts">
import { ref, watchEffect } from 'vue';
import { usePage } from '@inertiajs/vue3';
const page = usePage();
const page = usePage<{
jetstream: {
flash: {
banner: string;
bannerStyle: string;
};
};
}>();
const show = ref(true);
const style = ref('success');
const message = ref('');
@@ -16,17 +24,47 @@ watchEffect(async () => {
<template>
<div>
<div v-if="show && message" :class="{ 'bg-indigo-500': style == 'success', 'bg-red-700': style == 'danger' }">
<div
v-if="show && message"
:class="{
'bg-indigo-500': style == 'success',
'bg-red-700': style == 'danger',
}">
<div class="max-w-screen-xl mx-auto py-2 px-3 sm:px-6 lg:px-8">
<div class="flex items-center justify-between flex-wrap">
<div class="w-0 flex-1 flex items-center min-w-0">
<span class="flex p-2 rounded-lg" :class="{ 'bg-indigo-600': style == 'success', 'bg-red-600': style == 'danger' }">
<svg v-if="style == 'success'" class="h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<span
class="flex p-2 rounded-lg"
:class="{
'bg-indigo-600': style == 'success',
'bg-red-600': style == 'danger',
}">
<svg
v-if="style == 'success'"
class="h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<svg v-if="style == 'danger'" class="h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
<svg
v-if="style == 'danger'"
class="h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
</span>
@@ -39,12 +77,25 @@ watchEffect(async () => {
<button
type="button"
class="-me-1 flex p-2 rounded-md focus:outline-none sm:-me-2 transition"
:class="{ 'hover:bg-indigo-600 focus:bg-indigo-600': style == 'success', 'hover:bg-red-600 focus:bg-red-600': style == 'danger' }"
:class="{
'hover:bg-indigo-600 focus:bg-indigo-600':
style == 'success',
'hover:bg-red-600 focus:bg-red-600':
style == 'danger',
}"
aria-label="Dismiss"
@click.prevent="show = false"
>
<svg class="h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
@click.prevent="show = false">
<svg
class="h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { computed } from 'vue';
const emit = defineEmits(['update:checked']);
@@ -25,12 +25,10 @@ const proxyChecked = computed({
});
</script>
<template>
<input
v-model="proxyChecked"
type="checkbox"
:value="value"
class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800"
>
class="rounded dark:bg-gray-900 border-gray-300 dark:border-gray-700 text-indigo-600 shadow-sm focus:ring-indigo-500 dark:focus:ring-indigo-600 dark:focus:ring-offset-gray-800" />
</template>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import Modal from './Modal.vue';
const emit = defineEmits(['close']);
@@ -28,18 +28,28 @@ const close = () => {
:show="show"
:max-width="maxWidth"
:closeable="closeable"
@close="close"
>
@close="close">
<div class="bg-white dark:bg-gray-800 px-4 pt-5 pb-4 sm:p-6 sm:pb-4">
<div class="sm:flex sm:items-start">
<div class="mx-auto shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg class="h-6 w-6 text-red-600 dark:text-red-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
<div
class="mx-auto shrink-0 flex items-center justify-center h-12 w-12 rounded-full bg-red-100 sm:mx-0 sm:h-10 sm:w-10">
<svg
class="h-6 w-6 text-red-600 dark:text-red-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ms-4 sm:text-start">
<h3 class="text-lg font-medium text-gray-900 dark:text-gray-100">
<h3
class="text-lg font-medium text-gray-900 dark:text-gray-100">
<slot name="title" />
</h3>
@@ -50,7 +60,8 @@ const close = () => {
</div>
</div>
<div class="flex flex-row justify-end px-6 py-4 bg-gray-100 dark:bg-gray-800 text-end">
<div
class="flex flex-row justify-end px-6 py-4 bg-gray-100 dark:bg-gray-800 text-end">
<slot name="footer" />
</div>
</Modal>

View File

@@ -1,10 +1,11 @@
<script setup>
<script setup lang="ts">
import { ref, reactive, nextTick } from 'vue';
import DialogModal from './DialogModal.vue';
import InputError from './InputError.vue';
import PrimaryButton from './PrimaryButton.vue';
import SecondaryButton from './SecondaryButton.vue';
import TextInput from './TextInput.vue';
import axios from 'axios';
const emit = defineEmits(['confirmed']);
@@ -31,16 +32,16 @@ const form = reactive({
processing: false,
});
const passwordInput = ref(null);
const passwordInput = ref<HTMLInputElement | null>(null);
const startConfirmingPassword = () => {
axios.get(route('password.confirmation')).then(response => {
axios.get(route('password.confirmation')).then((response) => {
if (response.data.confirmed) {
emit('confirmed');
} else {
confirmingPassword.value = true;
setTimeout(() => passwordInput.value.focus(), 250);
setTimeout(() => passwordInput.value?.focus(), 250);
}
});
};
@@ -48,19 +49,21 @@ const startConfirmingPassword = () => {
const confirmPassword = () => {
form.processing = true;
axios.post(route('password.confirm'), {
password: form.password,
}).then(() => {
form.processing = false;
axios
.post(route('password.confirm'), {
password: form.password,
})
.then(() => {
form.processing = false;
closeModal();
nextTick().then(() => emit('confirmed'));
}).catch(error => {
form.processing = false;
form.error = error.response.data.errors.password[0];
passwordInput.value.focus();
});
closeModal();
nextTick().then(() => emit('confirmed'));
})
.catch((error) => {
form.processing = false;
form.error = error.response.data.errors.password[0];
passwordInput.value?.focus();
});
};
const closeModal = () => {
@@ -92,24 +95,20 @@ const closeModal = () => {
class="mt-1 block w-3/4"
placeholder="Password"
autocomplete="current-password"
@keyup.enter="confirmPassword"
/>
@keyup.enter="confirmPassword" />
<InputError :message="form.error" class="mt-2" />
</div>
</template>
<template #footer>
<SecondaryButton @click="closeModal">
Cancel
</SecondaryButton>
<SecondaryButton @click="closeModal"> Cancel </SecondaryButton>
<PrimaryButton
class="ms-3"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="confirmPassword"
>
@click="confirmPassword">
{{ button }}
</PrimaryButton>
</template>

View File

@@ -1,14 +1,20 @@
<script setup>
defineProps({
type: {
type: String,
default: 'button',
},
});
</script>
<script setup lang="ts">
import type { HtmlButtonType } from '@/types/dom';
withDefaults(
defineProps<{
type?: HtmlButtonType;
}>(),
{
type: 'button',
}
);
</script>
Modal.vue
<template>
<button :type="type" class="inline-flex items-center justify-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150">
<button
:type="type"
class="inline-flex items-center justify-center px-4 py-2 bg-red-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-red-500 active:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150">
<slot />
</button>
</template>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import Modal from './Modal.vue';
const emit = defineEmits(['close']);
@@ -28,8 +28,7 @@ const close = () => {
:show="show"
:max-width="maxWidth"
:closeable="closeable"
@close="close"
>
@close="close">
<div class="px-6 py-4">
<div class="text-lg font-medium text-gray-900 dark:text-gray-100">
<slot name="title" />
@@ -40,7 +39,8 @@ const close = () => {
</div>
</div>
<div class="flex flex-row justify-end px-6 py-4 bg-gray-100 dark:bg-gray-800 text-end">
<div
class="flex flex-row justify-end px-6 py-4 bg-gray-100 dark:bg-gray-800 text-end">
<slot name="footer" />
</div>
</Modal>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from 'vue';
const props = defineProps({
@@ -16,9 +16,9 @@ const props = defineProps({
},
});
let open = ref(false);
const open = ref(false);
const closeOnEscape = (e) => {
const closeOnEscape = (e: KeyboardEvent) => {
if (open.value && e.key === 'Escape') {
open.value = false;
}
@@ -29,7 +29,7 @@ onUnmounted(() => document.removeEventListener('keydown', closeOnEscape));
const widthClass = computed(() => {
return {
'48': 'w-48',
48: 'w-48',
}[props.width.toString()];
});
@@ -48,7 +48,7 @@ const alignmentClasses = computed(() => {
<template>
<div class="relative">
<div @click="open = ! open">
<div @click="open = !open">
<slot name="trigger" />
</div>
@@ -61,16 +61,16 @@ const alignmentClasses = computed(() => {
enter-to-class="transform opacity-100 scale-100"
leave-active-class="transition ease-in duration-75"
leave-from-class="transform opacity-100 scale-100"
leave-to-class="transform opacity-0 scale-95"
>
leave-to-class="transform opacity-0 scale-95">
<div
v-show="open"
class="absolute z-50 mt-2 rounded-md shadow-lg"
:class="[widthClass, alignmentClasses]"
style="display: none;"
@click="open = false"
>
<div class="rounded-md ring-1 ring-black ring-opacity-5" :class="contentClasses">
style="display: none"
@click="open = false">
<div
class="rounded-md ring-1 ring-black ring-opacity-5"
:class="contentClasses">
<slot name="content" />
</div>
</div>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { Link } from '@inertiajs/vue3';
defineProps({
@@ -9,15 +9,24 @@ defineProps({
<template>
<div>
<button v-if="as == 'button'" type="submit" class="block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out">
<button
v-if="as == 'button'"
type="submit"
class="block w-full px-4 py-2 text-start text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out">
<slot />
</button>
<a v-else-if="as =='a'" :href="href" class="block px-4 py-2 text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out">
<a
v-else-if="as == 'a'"
:href="href"
class="block px-4 py-2 text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out">
<slot />
</a>
<Link v-else :href="href" class="block px-4 py-2 text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out">
<Link
v-else
:href="href ?? ''"
class="block px-4 py-2 text-sm leading-5 text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-800 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-800 transition duration-150 ease-in-out">
<slot />
</Link>
</div>

View File

@@ -1,10 +1,10 @@
<script setup>
<script setup lang="ts">
import { computed, useSlots } from 'vue';
import SectionTitle from './SectionTitle.vue';
defineEmits(['submitted']);
const hasActions = computed(() => !! useSlots().actions);
const hasActions = computed(() => !!useSlots().actions);
</script>
<template>
@@ -22,14 +22,19 @@ const hasActions = computed(() => !! useSlots().actions);
<form @submit.prevent="$emit('submitted')">
<div
class="px-4 py-5 bg-white dark:bg-gray-800 sm:p-6 shadow"
:class="hasActions ? 'sm:rounded-tl-md sm:rounded-tr-md' : 'sm:rounded-md'"
>
:class="
hasActions
? 'sm:rounded-tl-md sm:rounded-tr-md'
: 'sm:rounded-md'
">
<div class="grid grid-cols-6 gap-6">
<slot name="form" />
</div>
</div>
<div v-if="hasActions" class="flex items-center justify-end px-4 py-3 bg-gray-50 dark:bg-gray-800 text-end sm:px-6 shadow sm:rounded-bl-md sm:rounded-br-md">
<div
v-if="hasActions"
class="flex items-center justify-end px-4 py-3 bg-gray-50 dark:bg-gray-800 text-end sm:px-6 shadow sm:rounded-bl-md sm:rounded-br-md">
<slot name="actions" />
</div>
</form>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
defineProps({
message: String,
});

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
defineProps({
value: String,
});

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, watch } from 'vue';
const props = defineProps({
@@ -18,13 +18,16 @@ const props = defineProps({
const emit = defineEmits(['close']);
watch(() => props.show, () => {
if (props.show) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = null;
watch(
() => props.show,
() => {
if (props.show) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'visible';
}
}
});
);
const close = () => {
if (props.closeable) {
@@ -32,7 +35,7 @@ const close = () => {
}
};
const closeOnEscape = (e) => {
const closeOnEscape = (e: KeyboardEvent) => {
if (e.key === 'Escape' && props.show) {
close();
}
@@ -42,15 +45,15 @@ onMounted(() => document.addEventListener('keydown', closeOnEscape));
onUnmounted(() => {
document.removeEventListener('keydown', closeOnEscape);
document.body.style.overflow = null;
document.body.style.overflow = 'visible';
});
const maxWidthClass = computed(() => {
return {
'sm': 'sm:max-w-sm',
'md': 'sm:max-w-md',
'lg': 'sm:max-w-lg',
'xl': 'sm:max-w-xl',
sm: 'sm:max-w-sm',
md: 'sm:max-w-md',
lg: 'sm:max-w-lg',
xl: 'sm:max-w-xl',
'2xl': 'sm:max-w-2xl',
}[props.maxWidth];
});
@@ -59,17 +62,23 @@ const maxWidthClass = computed(() => {
<template>
<teleport to="body">
<transition leave-active-class="duration-200">
<div v-show="show" class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50" scroll-region>
<div
v-show="show"
class="fixed inset-0 overflow-y-auto px-4 py-6 sm:px-0 z-50"
scroll-region>
<transition
enter-active-class="ease-out duration-300"
enter-from-class="opacity-0"
enter-to-class="opacity-100"
leave-active-class="ease-in duration-200"
leave-from-class="opacity-100"
leave-to-class="opacity-0"
>
<div v-show="show" class="fixed inset-0 transform transition-all" @click="close">
<div class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75" />
leave-to-class="opacity-0">
<div
v-show="show"
class="fixed inset-0 transform transition-all"
@click="close">
<div
class="absolute inset-0 bg-gray-500 dark:bg-gray-900 opacity-75" />
</div>
</transition>
@@ -79,9 +88,11 @@ const maxWidthClass = computed(() => {
enter-to-class="opacity-100 translate-y-0 sm:scale-100"
leave-active-class="ease-in duration-200"
leave-from-class="opacity-100 translate-y-0 sm:scale-100"
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
>
<div v-show="show" class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full sm:mx-auto" :class="maxWidthClass">
leave-to-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95">
<div
v-show="show"
class="mb-6 bg-white dark:bg-gray-800 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full sm:mx-auto"
:class="maxWidthClass">
<slot v-if="show" />
</div>
</transition>

View File

@@ -1,11 +1,11 @@
<script setup>
<script setup lang="ts">
import { computed } from 'vue';
import { Link } from '@inertiajs/vue3';
const props = defineProps({
href: String,
active: Boolean,
});
const props = defineProps<{
href?: string;
active?: boolean;
}>();
const classes = computed(() => {
return props.active
@@ -15,7 +15,7 @@ const classes = computed(() => {
</script>
<template>
<Link :href="href" :class="classes">
<Link :href="href ?? ''" :class="classes">
<slot />
</Link>
</template>

View File

@@ -1,14 +1,20 @@
<script setup>
defineProps({
type: {
type: String,
default: 'submit',
},
});
<script setup lang="ts">
import type { HtmlButtonType } from '@/types/dom';
withDefaults(
defineProps<{
type: HtmlButtonType;
}>(),
{
type: 'submit',
}
);
</script>
<template>
<button :type="type" class="inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150">
<button
:type="type"
class="inline-flex items-center px-4 py-2 bg-gray-800 dark:bg-gray-200 border border-transparent rounded-md font-semibold text-xs text-white dark:text-gray-800 uppercase tracking-widest hover:bg-gray-700 dark:hover:bg-white focus:bg-gray-700 dark:focus:bg-white active:bg-gray-900 dark:active:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 transition ease-in-out duration-150">
<slot />
</button>
</template>

View File

@@ -1,12 +1,12 @@
<script setup>
<script setup lang="ts">
import { computed } from 'vue';
import { Link } from '@inertiajs/vue3';
const props = defineProps({
active: Boolean,
href: String,
as: String,
});
const props = defineProps<{
active?: boolean;
href?: string;
as?: string;
}>();
const classes = computed(() => {
return props.active
@@ -17,11 +17,14 @@ const classes = computed(() => {
<template>
<div>
<button v-if="as == 'button'" :class="classes" class="w-full text-start">
<button
v-if="as == 'button'"
:class="classes"
class="w-full text-start">
<slot />
</button>
<Link v-else :href="href" :class="classes">
<Link v-else :href="href ?? ''" :class="classes">
<slot />
</Link>
</div>

View File

@@ -1,14 +1,21 @@
<script setup>
defineProps({
type: {
type: String,
default: 'button',
},
});
<script setup lang="ts">
import type { HtmlButtonType } from '@/types/dom';
withDefaults(
defineProps<{
type: HtmlButtonType;
}>(),
{
type: 'button',
}
);
</script>
<template>
<button :type="type" class="inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150">
<button
:type="type"
ResponsiveNavLink.vue
class="inline-flex items-center px-4 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-500 rounded-md font-semibold text-xs text-gray-700 dark:text-gray-300 uppercase tracking-widest shadow-sm hover:bg-gray-50 dark:hover:bg-gray-700 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 dark:focus:ring-offset-gray-800 disabled:opacity-25 transition ease-in-out duration-150">
<slot />
</button>
</template>

View File

@@ -1,21 +1,27 @@
<script setup>
<script setup lang="ts">
import { onMounted, ref } from 'vue';
defineProps({
modelValue: String,
});
defineEmits(['update:modelValue']);
const input = ref(null);
const input = ref<HTMLInputElement | null>(null);
onMounted(() => {
if (input.value.hasAttribute('autofocus')) {
input.value.focus();
if (input.value?.hasAttribute('autofocus')) {
input.value?.focus();
}
});
defineExpose({ focus: () => input.value.focus() });
defineExpose({ focus: () => input.value?.focus() });
const emit = defineEmits(['update:modelValue']);
function updateValue(event: Event) {
if (event.target && 'value' in event.target) {
emit('update:modelValue', event?.target?.value);
}
}
</script>
<template>
@@ -23,6 +29,5 @@ defineExpose({ focus: () => input.value.focus() });
ref="input"
class="border-gray-300 dark:border-gray-700 dark:bg-gray-900 dark:text-gray-300 focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-indigo-500 dark:focus:ring-indigo-600 rounded-md shadow-sm"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
@input="updateValue" />
</template>

View File

@@ -1,10 +1,11 @@
<script setup>
<script setup lang="ts">
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
</script>
<template>
<div>
<div class="p-6 lg:p-8 bg-white dark:bg-gray-800 dark:bg-gradient-to-bl dark:from-gray-700/50 dark:via-transparent border-b border-gray-200 dark:border-gray-700">
<div
class="p-6 lg:p-8 bg-white dark:bg-gray-800 dark:bg-gradient-to-bl dark:from-gray-700/50 dark:via-transparent border-b border-gray-200 dark:border-gray-700">
<ApplicationLogo class="block h-12 w-auto" />
<h1 class="mt-8 text-2xl font-medium text-gray-900 dark:text-white">
@@ -12,34 +13,59 @@ import ApplicationLogo from '@/Components/ApplicationLogo.vue';
</h1>
<p class="mt-6 text-gray-500 dark:text-gray-400 leading-relaxed">
Laravel Jetstream provides a beautiful, robust starting point for your next Laravel application. Laravel is designed
to help you build your application using a development environment that is simple, powerful, and enjoyable. We believe
you should love expressing your creativity through programming, so we have spent time carefully crafting the Laravel
ecosystem to be a breath of fresh air. We hope you love it.
Laravel Jetstream provides a beautiful, robust starting point
for your next Laravel application. Laravel is designed to help
you build your application using a development environment that
is simple, powerful, and enjoyable. We believe you should love
expressing your creativity through programming, so we have spent
time carefully crafting the Laravel ecosystem to be a breath of
fresh air. We hope you love it.
</p>
</div>
<div class="bg-gray-200 dark:bg-gray-800 bg-opacity-25 grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8 p-6 lg:p-8">
<div
class="bg-gray-200 dark:bg-gray-800 bg-opacity-25 grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8 p-6 lg:p-8">
<div>
<div class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-6 h-6 stroke-gray-400">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
class="w-6 h-6 stroke-gray-400">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
</svg>
<h2 class="ms-3 text-xl font-semibold text-gray-900 dark:text-white">
<h2
class="ms-3 text-xl font-semibold text-gray-900 dark:text-white">
<a href="https://laravel.com/docs">Documentation</a>
</h2>
</div>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laravel has wonderful documentation covering every aspect of the framework. Whether you're new to the framework or have previous experience, we recommend reading all of the documentation from beginning to end.
<p
class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laravel has wonderful documentation covering every aspect of
the framework. Whether you're new to the framework or have
previous experience, we recommend reading all of the
documentation from beginning to end.
</p>
<p class="mt-4 text-sm">
<a href="https://laravel.com/docs" class="inline-flex items-center font-semibold text-indigo-700 dark:text-indigo-300">
<a
href="https://laravel.com/docs"
class="inline-flex items-center font-semibold text-indigo-700 dark:text-indigo-300">
Explore the documentation
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="ms-1 w-5 h-5 fill-indigo-500 dark:fill-indigo-200">
<path fill-rule="evenodd" d="M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z" clip-rule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
class="ms-1 w-5 h-5 fill-indigo-500 dark:fill-indigo-200">
<path
fill-rule="evenodd"
d="M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z"
clip-rule="evenodd" />
</svg>
</a>
</p>
@@ -47,24 +73,44 @@ import ApplicationLogo from '@/Components/ApplicationLogo.vue';
<div>
<div class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-6 h-6 stroke-gray-400">
<path stroke-linecap="round" d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
class="w-6 h-6 stroke-gray-400">
<path
stroke-linecap="round"
d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z" />
</svg>
<h2 class="ms-3 text-xl font-semibold text-gray-900 dark:text-white">
<h2
class="ms-3 text-xl font-semibold text-gray-900 dark:text-white">
<a href="https://laracasts.com">Laracasts</a>
</h2>
</div>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.
<p
class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laracasts offers thousands of video tutorials on Laravel,
PHP, and JavaScript development. Check them out, see for
yourself, and massively level up your development skills in
the process.
</p>
<p class="mt-4 text-sm">
<a href="https://laracasts.com" class="inline-flex items-center font-semibold text-indigo-700 dark:text-indigo-300">
<a
href="https://laracasts.com"
class="inline-flex items-center font-semibold text-indigo-700 dark:text-indigo-300">
Start watching Laracasts
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="ms-1 w-5 h-5 fill-indigo-500 dark:fill-indigo-200">
<path fill-rule="evenodd" d="M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z" clip-rule="evenodd" />
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
class="ms-1 w-5 h-5 fill-indigo-500 dark:fill-indigo-200">
<path
fill-rule="evenodd"
d="M5 10a.75.75 0 01.75-.75h6.638L10.23 7.29a.75.75 0 111.04-1.08l3.5 3.25a.75.75 0 010 1.08l-3.5 3.25a.75.75 0 11-1.04-1.08l2.158-1.96H5.75A.75.75 0 015 10z"
clip-rule="evenodd" />
</svg>
</a>
</p>
@@ -72,31 +118,58 @@ import ApplicationLogo from '@/Components/ApplicationLogo.vue';
<div>
<div class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-6 h-6 stroke-gray-400">
<path stroke-linecap="round" stroke-linejoin="round" d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
class="w-6 h-6 stroke-gray-400">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M2.25 15.75l5.159-5.159a2.25 2.25 0 013.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 013.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 001.5-1.5V6a1.5 1.5 0 00-1.5-1.5H3.75A1.5 1.5 0 002.25 6v12a1.5 1.5 0 001.5 1.5zm10.5-11.25h.008v.008h-.008V8.25zm.375 0a.375.375 0 11-.75 0 .375.375 0 01.75 0z" />
</svg>
<h2 class="ms-3 text-xl font-semibold text-gray-900 dark:text-white">
<h2
class="ms-3 text-xl font-semibold text-gray-900 dark:text-white">
<a href="https://tailwindcss.com/">Tailwind</a>
</h2>
</div>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laravel Jetstream is built with Tailwind, an amazing utility first CSS framework that doesn't get in your way. You'll be amazed how easily you can build and maintain fresh, modern designs with this wonderful framework at your fingertips.
<p
class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laravel Jetstream is built with Tailwind, an amazing utility
first CSS framework that doesn't get in your way. You'll be
amazed how easily you can build and maintain fresh, modern
designs with this wonderful framework at your fingertips.
</p>
</div>
<div>
<div class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-6 h-6 stroke-gray-400">
<path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
class="w-6 h-6 stroke-gray-400">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.5 10.5V6.75a4.5 4.5 0 10-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 002.25-2.25v-6.75a2.25 2.25 0 00-2.25-2.25H6.75a2.25 2.25 0 00-2.25 2.25v6.75a2.25 2.25 0 002.25 2.25z" />
</svg>
<h2 class="ms-3 text-xl font-semibold text-gray-900 dark:text-white">
<h2
class="ms-3 text-xl font-semibold text-gray-900 dark:text-white">
Authentication
</h2>
</div>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Authentication and registration views are included with Laravel Jetstream, as well as support for user email verification and resetting forgotten passwords. So, you're free to get started with what matters most: building your application.
<p
class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Authentication and registration views are included with
Laravel Jetstream, as well as support for user email
verification and resetting forgotten passwords. So, you're
free to get started with what matters most: building your
application.
</p>
</div>
</div>

View File

@@ -1,12 +1,14 @@
<script setup>
<script setup lang="ts">
import { ref } from 'vue';
import { Head, Link, router } from '@inertiajs/vue3';
import { Head, Link, router, usePage } from '@inertiajs/vue3';
import ApplicationMark from '@/Components/ApplicationMark.vue';
import Banner from '@/Components/Banner.vue';
import Dropdown from '@/Components/Dropdown.vue';
import DropdownLink from '@/Components/DropdownLink.vue';
import NavLink from '@/Components/NavLink.vue';
import ResponsiveNavLink from '@/Components/ResponsiveNavLink.vue';
import route from 'ziggy-js';
import type { Organization, User } from '@/types/models';
defineProps({
title: String,
@@ -14,12 +16,28 @@ defineProps({
const showingNavigationDropdown = ref(false);
const switchToTeam = (team) => {
router.put(route('current-team.update'), {
team_id: team.id,
}, {
preserveState: false,
});
const page = usePage<{
jetstream: {
canCreateTeams: boolean;
hasTeamFeatures: boolean;
managesProfilePhotos: boolean;
hasApiFeatures: boolean;
};
auth: {
user: User & { all_teams: Organization[] };
};
}>();
const switchToTeam = (team: Organization) => {
router.put(
route('current-team.update'),
{
team_id: team.id,
},
{
preserveState: false,
}
);
};
const logout = () => {
@@ -34,7 +52,8 @@ const logout = () => {
<Banner />
<div class="min-h-screen bg-gray-100 dark:bg-gray-900">
<nav class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700">
<nav
class="bg-white dark:bg-gray-800 border-b border-gray-100 dark:border-gray-700">
<!-- Primary Navigation Menu -->
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between h-16">
@@ -47,8 +66,11 @@ const logout = () => {
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<NavLink :href="route('dashboard')" :active="route().current('dashboard')">
<div
class="hidden space-x-8 sm:-my-px sm:ms-10 sm:flex">
<NavLink
:href="route('dashboard')"
:active="route().current('dashboard')">
Dashboard
</NavLink>
</div>
@@ -57,14 +79,31 @@ const logout = () => {
<div class="hidden sm:flex sm:items-center sm:ms-6">
<div class="ms-3 relative">
<!-- Teams Dropdown -->
<Dropdown v-if="$page.props.jetstream.hasTeamFeatures" align="right" width="60">
<Dropdown
v-if="page.props.jetstream.hasTeamFeatures"
align="right"
width="60">
<template #trigger>
<span class="inline-flex rounded-md">
<button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none focus:bg-gray-50 dark:focus:bg-gray-700 active:bg-gray-50 dark:active:bg-gray-700 transition ease-in-out duration-150">
{{ $page.props.auth.user.current_team.name }}
<button
type="button"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none focus:bg-gray-50 dark:focus:bg-gray-700 active:bg-gray-50 dark:active:bg-gray-700 transition ease-in-out duration-150">
{{
page.props.auth.user
.current_team.name
}}
<svg class="ms-2 -me-0.5 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
<svg
class="ms-2 -me-0.5 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M8.25 15L12 18.75 15.75 15m-7.5-6L12 5.25 15.75 9" />
</svg>
</button>
</span>
@@ -73,36 +112,84 @@ const logout = () => {
<template #content>
<div class="w-60">
<!-- Organization Management -->
<div class="block px-4 py-2 text-xs text-gray-400">
<div
class="block px-4 py-2 text-xs text-gray-400">
Manage Team
</div>
<!-- Organization Settings -->
<DropdownLink :href="route('teams.show', $page.props.auth.user.current_team)">
<DropdownLink
:href="
route(
'teams.show',
page.props.auth.user
.current_team.id
)
">
Team Settings
</DropdownLink>
<DropdownLink v-if="$page.props.jetstream.canCreateTeams" :href="route('teams.create')">
<DropdownLink
v-if="
page.props.jetstream
.canCreateTeams
"
:href="route('teams.create')">
Create New Team
</DropdownLink>
<!-- Organization Switcher -->
<template v-if="$page.props.auth.user.all_teams.length > 1">
<div class="border-t border-gray-200 dark:border-gray-600" />
<template
v-if="
page.props.auth.user
.all_teams.length > 1
">
<div
class="border-t border-gray-200 dark:border-gray-600" />
<div class="block px-4 py-2 text-xs text-gray-400">
<div
class="block px-4 py-2 text-xs text-gray-400">
Switch Teams
</div>
<template v-for="team in $page.props.auth.user.all_teams" :key="team.id">
<form @submit.prevent="switchToTeam(team)">
<DropdownLink as="button">
<div class="flex items-center">
<svg v-if="team.id == $page.props.auth.user.current_team_id" class="me-2 h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<template
v-for="team in page.props
.auth.user.all_teams"
:key="team.id">
<form
@submit.prevent="
switchToTeam(team)
">
<DropdownLink
as="button">
<div
class="flex items-center">
<svg
v-if="
team.id ==
page
.props
.auth
.user
.current_team_id
"
class="me-2 h-5 w-5 text-green-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>{{ team.name }}</div>
<div>
{{
team.name
}}
</div>
</div>
</DropdownLink>
</form>
@@ -117,16 +204,42 @@ const logout = () => {
<div class="ms-3 relative">
<Dropdown align="right" width="48">
<template #trigger>
<button v-if="$page.props.jetstream.managesProfilePhotos" class="flex text-sm border-2 border-transparent rounded-full focus:outline-none focus:border-gray-300 transition">
<img class="h-8 w-8 rounded-full object-cover" :src="$page.props.auth.user.profile_photo_url" :alt="$page.props.auth.user.name">
<button
v-if="
page.props.jetstream
.managesProfilePhotos
"
class="flex text-sm border-2 border-transparent rounded-full focus:outline-none focus:border-gray-300 transition">
<img
class="h-8 w-8 rounded-full object-cover"
:src="
page.props.auth.user
.profile_photo_url
"
:alt="
page.props.auth.user.name
" />
</button>
<span v-else class="inline-flex rounded-md">
<button type="button" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none focus:bg-gray-50 dark:focus:bg-gray-700 active:bg-gray-50 dark:active:bg-gray-700 transition ease-in-out duration-150">
{{ $page.props.auth.user.name }}
<span
v-else
class="inline-flex rounded-md">
<button
type="button"
class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 dark:text-gray-400 bg-white dark:bg-gray-800 hover:text-gray-700 dark:hover:text-gray-300 focus:outline-none focus:bg-gray-50 dark:focus:bg-gray-700 active:bg-gray-50 dark:active:bg-gray-700 transition ease-in-out duration-150">
{{ page.props.auth.user.name }}
<svg class="ms-2 -me-0.5 h-4 w-4" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
<svg
class="ms-2 -me-0.5 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</button>
</span>
@@ -134,19 +247,27 @@ const logout = () => {
<template #content>
<!-- Account Management -->
<div class="block px-4 py-2 text-xs text-gray-400">
<div
class="block px-4 py-2 text-xs text-gray-400">
Manage Account
</div>
<DropdownLink :href="route('profile.show')">
<DropdownLink
:href="route('profile.show')">
Profile
</DropdownLink>
<DropdownLink v-if="$page.props.jetstream.hasApiFeatures" :href="route('api-tokens.index')">
<DropdownLink
v-if="
page.props.jetstream
.hasApiFeatures
"
:href="route('api-tokens.index')">
API Tokens
</DropdownLink>
<div class="border-t border-gray-200 dark:border-gray-600" />
<div
class="border-t border-gray-200 dark:border-gray-600" />
<!-- Authentication -->
<form @submit.prevent="logout">
@@ -161,27 +282,37 @@ const logout = () => {
<!-- Hamburger -->
<div class="-me-2 flex items-center sm:hidden">
<button class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out" @click="showingNavigationDropdown = ! showingNavigationDropdown">
<button
class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 dark:text-gray-500 hover:text-gray-500 dark:hover:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-900 focus:outline-none focus:bg-gray-100 dark:focus:bg-gray-900 focus:text-gray-500 dark:focus:text-gray-400 transition duration-150 ease-in-out"
@click="
showingNavigationDropdown =
!showingNavigationDropdown
">
<svg
class="h-6 w-6"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
viewBox="0 0 24 24">
<path
:class="{'hidden': showingNavigationDropdown, 'inline-flex': ! showingNavigationDropdown }"
:class="{
hidden: showingNavigationDropdown,
'inline-flex':
!showingNavigationDropdown,
}"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
d="M4 6h16M4 12h16M4 18h16" />
<path
:class="{'hidden': ! showingNavigationDropdown, 'inline-flex': showingNavigationDropdown }"
:class="{
hidden: !showingNavigationDropdown,
'inline-flex':
showingNavigationDropdown,
}"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
@@ -189,36 +320,57 @@ const logout = () => {
</div>
<!-- Responsive Navigation Menu -->
<div :class="{'block': showingNavigationDropdown, 'hidden': ! showingNavigationDropdown}" class="sm:hidden">
<div
:class="{
block: showingNavigationDropdown,
hidden: !showingNavigationDropdown,
}"
class="sm:hidden">
<div class="pt-2 pb-3 space-y-1">
<ResponsiveNavLink :href="route('dashboard')" :active="route().current('dashboard')">
<ResponsiveNavLink
:href="route('dashboard')"
:active="route().current('dashboard')">
Dashboard
</ResponsiveNavLink>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
<div
class="pt-4 pb-1 border-t border-gray-200 dark:border-gray-600">
<div class="flex items-center px-4">
<div v-if="$page.props.jetstream.managesProfilePhotos" class="shrink-0 me-3">
<img class="h-10 w-10 rounded-full object-cover" :src="$page.props.auth.user.profile_photo_url" :alt="$page.props.auth.user.name">
<div
v-if="page.props.jetstream.managesProfilePhotos"
class="shrink-0 me-3">
<img
class="h-10 w-10 rounded-full object-cover"
:src="
page.props.auth.user.profile_photo_url
"
:alt="page.props.auth.user.name" />
</div>
<div>
<div class="font-medium text-base text-gray-800 dark:text-gray-200">
{{ $page.props.auth.user.name }}
<div
class="font-medium text-base text-gray-800 dark:text-gray-200">
{{ page.props.auth.user.name }}
</div>
<div class="font-medium text-sm text-gray-500">
{{ $page.props.auth.user.email }}
{{ page.props.auth.user.email }}
</div>
</div>
</div>
<div class="mt-3 space-y-1">
<ResponsiveNavLink :href="route('profile.show')" :active="route().current('profile.show')">
<ResponsiveNavLink
:href="route('profile.show')"
:active="route().current('profile.show')">
Profile
</ResponsiveNavLink>
<ResponsiveNavLink v-if="$page.props.jetstream.hasApiFeatures" :href="route('api-tokens.index')" :active="route().current('api-tokens.index')">
<ResponsiveNavLink
v-if="page.props.jetstream.hasApiFeatures"
:href="route('api-tokens.index')"
:active="route().current('api-tokens.index')">
API Tokens
</ResponsiveNavLink>
@@ -230,36 +382,75 @@ const logout = () => {
</form>
<!-- Organization Management -->
<template v-if="$page.props.jetstream.hasTeamFeatures">
<div class="border-t border-gray-200 dark:border-gray-600" />
<template
v-if="page.props.jetstream.hasTeamFeatures">
<div
class="border-t border-gray-200 dark:border-gray-600" />
<div class="block px-4 py-2 text-xs text-gray-400">
<div
class="block px-4 py-2 text-xs text-gray-400">
Manage Team
</div>
<!-- Organization Settings -->
<ResponsiveNavLink :href="route('teams.show', $page.props.auth.user.current_team)" :active="route().current('teams.show')">
<ResponsiveNavLink
:href="
route(
'teams.show',
page.props.auth.user.current_team.id
)
"
:active="route().current('teams.show')">
Team Settings
</ResponsiveNavLink>
<ResponsiveNavLink v-if="$page.props.jetstream.canCreateTeams" :href="route('teams.create')" :active="route().current('teams.create')">
<ResponsiveNavLink
v-if="page.props.jetstream.canCreateTeams"
:href="route('teams.create')"
:active="route().current('teams.create')">
Create New Team
</ResponsiveNavLink>
<!-- Organization Switcher -->
<template v-if="$page.props.auth.user.all_teams.length > 1">
<div class="border-t border-gray-200 dark:border-gray-600" />
<template
v-if="
page.props.auth.user.all_teams.length >
1
">
<div
class="border-t border-gray-200 dark:border-gray-600" />
<div class="block px-4 py-2 text-xs text-gray-400">
<div
class="block px-4 py-2 text-xs text-gray-400">
Switch Teams
</div>
<template v-for="team in $page.props.auth.user.all_teams" :key="team.id">
<form @submit.prevent="switchToTeam(team)">
<template
v-for="team in page.props.auth.user
.all_teams"
:key="team.id">
<form
@submit.prevent="
switchToTeam(team)
">
<ResponsiveNavLink as="button">
<div class="flex items-center">
<svg v-if="team.id == $page.props.auth.user.current_team_id" class="me-2 h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
v-if="
team.id ==
page.props.auth.user
.current_team_id
"
class="me-2 h-5 w-5 text-green-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>{{ team.name }}</div>
</div>
@@ -274,7 +465,9 @@ const logout = () => {
</nav>
<!-- Page Heading -->
<header v-if="$slots.header" class="bg-white dark:bg-gray-800 shadow">
<header
v-if="$slots.header"
class="bg-white dark:bg-gray-800 shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<slot name="header" />
</div>

View File

@@ -1,18 +1,20 @@
<script setup>
<script setup lang="ts">
import ApiTokenManager from '@/Pages/API/Partials/ApiTokenManager.vue';
import AppLayout from '@/Layouts/AppLayout.vue';
import type { Token } from '@/types/jetstream';
defineProps({
tokens: Array,
availablePermissions: Array,
defaultPermissions: Array,
});
defineProps<{
tokens: Token[];
availablePermissions: string[];
defaultPermissions: string[];
}>();
</script>
<template>
<AppLayout title="API Tokens">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
<h2
class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
API Tokens
</h2>
</template>
@@ -22,8 +24,7 @@ defineProps({
<ApiTokenManager
:tokens="tokens"
:available-permissions="availablePermissions"
:default-permissions="defaultPermissions"
/>
:default-permissions="defaultPermissions" />
</div>
</div>
</AppLayout>

View File

@@ -1,6 +1,6 @@
<script setup>
<script setup lang="ts">
import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3';
import { useForm, usePage } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import ActionSection from '@/Components/ActionSection.vue';
import Checkbox from '@/Components/Checkbox.vue';
@@ -14,27 +14,38 @@ import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import SectionBorder from '@/Components/SectionBorder.vue';
import TextInput from '@/Components/TextInput.vue';
import type { Token } from '@/types/jetstream';
const props = defineProps({
tokens: Array,
availablePermissions: Array,
defaultPermissions: Array,
});
const props = defineProps<{
tokens: Token[];
availablePermissions: string[];
defaultPermissions: string[];
}>();
const createApiTokenForm = useForm({
name: '',
permissions: props.defaultPermissions,
});
const updateApiTokenForm = useForm({
const page = usePage<{
jetstream: {
flash: {
token: string;
};
};
}>();
const updateApiTokenForm = useForm<{
permissions: string[];
}>({
permissions: [],
});
const deleteApiTokenForm = useForm({});
const displayingToken = ref(false);
const managingPermissionsFor = ref(null);
const apiTokenBeingDeleted = ref(null);
const managingPermissionsFor = ref<Token | null>(null);
const apiTokenBeingDeleted = ref<Token | null>(null);
const createApiToken = () => {
createApiTokenForm.post(route('api-tokens.store'), {
@@ -46,29 +57,35 @@ const createApiToken = () => {
});
};
const manageApiTokenPermissions = (token) => {
const manageApiTokenPermissions = (token: Token) => {
updateApiTokenForm.permissions = token.abilities;
managingPermissionsFor.value = token;
};
const updateApiToken = () => {
updateApiTokenForm.put(route('api-tokens.update', managingPermissionsFor.value), {
preserveScroll: true,
preserveState: true,
onSuccess: () => (managingPermissionsFor.value = null),
});
updateApiTokenForm.put(
route('api-tokens.update', managingPermissionsFor.value?.id),
{
preserveScroll: true,
preserveState: true,
onSuccess: () => (managingPermissionsFor.value = null),
}
);
};
const confirmApiTokenDeletion = (token) => {
const confirmApiTokenDeletion = (token: Token) => {
apiTokenBeingDeleted.value = token;
};
const deleteApiToken = () => {
deleteApiTokenForm.delete(route('api-tokens.destroy', apiTokenBeingDeleted.value), {
preserveScroll: true,
preserveState: true,
onSuccess: () => (apiTokenBeingDeleted.value = null),
});
deleteApiTokenForm.delete(
route('api-tokens.destroy', apiTokenBeingDeleted.value?.id),
{
preserveScroll: true,
preserveState: true,
onSuccess: () => (apiTokenBeingDeleted.value = null),
}
);
};
</script>
@@ -76,12 +93,11 @@ const deleteApiToken = () => {
<div>
<!-- Generate API Token -->
<FormSection @submitted="createApiToken">
<template #title>
Create API Token
</template>
<template #title> Create API Token </template>
<template #description>
API tokens allow third-party services to authenticate with our application on your behalf.
API tokens allow third-party services to authenticate with our
application on your behalf.
</template>
<template #form>
@@ -93,9 +109,10 @@ const deleteApiToken = () => {
v-model="createApiTokenForm.name"
type="text"
class="mt-1 block w-full"
autofocus
/>
<InputError :message="createApiTokenForm.errors.name" class="mt-2" />
autofocus />
<InputError
:message="createApiTokenForm.errors.name"
class="mt-2" />
</div>
<!-- Token Permissions -->
@@ -103,10 +120,19 @@ const deleteApiToken = () => {
<InputLabel for="permissions" value="Permissions" />
<div class="mt-2 grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="permission in availablePermissions" :key="permission">
<div
v-for="permission in availablePermissions"
:key="permission">
<label class="flex items-center">
<Checkbox v-model:checked="createApiTokenForm.permissions" :value="permission" />
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400">{{ permission }}</span>
<Checkbox
v-model:checked="
createApiTokenForm.permissions
"
:value="permission" />
<span
class="ms-2 text-sm text-gray-600 dark:text-gray-400"
>{{ permission }}</span
>
</label>
</div>
</div>
@@ -114,11 +140,15 @@ const deleteApiToken = () => {
</template>
<template #actions>
<ActionMessage :on="createApiTokenForm.recentlySuccessful" class="me-3">
<ActionMessage
:on="createApiTokenForm.recentlySuccessful"
class="me-3">
Created.
</ActionMessage>
<PrimaryButton :class="{ 'opacity-25': createApiTokenForm.processing }" :disabled="createApiTokenForm.processing">
<PrimaryButton
:class="{ 'opacity-25': createApiTokenForm.processing }"
:disabled="createApiTokenForm.processing">
Create
</PrimaryButton>
</template>
@@ -130,36 +160,43 @@ const deleteApiToken = () => {
<!-- Manage API Tokens -->
<div class="mt-10 sm:mt-0">
<ActionSection>
<template #title>
Manage API Tokens
</template>
<template #title> Manage API Tokens </template>
<template #description>
You may delete any of your existing tokens if they are no longer needed.
You may delete any of your existing tokens if they are
no longer needed.
</template>
<!-- API Token List -->
<template #content>
<div class="space-y-6">
<div v-for="token in tokens" :key="token.id" class="flex items-center justify-between">
<div
v-for="token in tokens"
:key="token.id"
class="flex items-center justify-between">
<div class="break-all dark:text-white">
{{ token.name }}
</div>
<div class="flex items-center ms-2">
<div v-if="token.last_used_ago" class="text-sm text-gray-400">
<div
v-if="token.last_used_ago"
class="text-sm text-gray-400">
Last used {{ token.last_used_ago }}
</div>
<button
v-if="availablePermissions.length > 0"
class="cursor-pointer ms-6 text-sm text-gray-400 underline"
@click="manageApiTokenPermissions(token)"
>
@click="
manageApiTokenPermissions(token)
">
Permissions
</button>
<button class="cursor-pointer ms-6 text-sm text-red-500" @click="confirmApiTokenDeletion(token)">
<button
class="cursor-pointer ms-6 text-sm text-red-500"
@click="confirmApiTokenDeletion(token)">
Delete
</button>
</div>
@@ -172,17 +209,18 @@ const deleteApiToken = () => {
<!-- Token Value Modal -->
<DialogModal :show="displayingToken" @close="displayingToken = false">
<template #title>
API Token
</template>
<template #title> API Token </template>
<template #content>
<div>
Please copy your new API token. For your security, it won't be shown again.
Please copy your new API token. For your security, it won't
be shown again.
</div>
<div v-if="$page.props.jetstream.flash.token" class="mt-4 bg-gray-100 dark:bg-gray-900 px-4 py-2 rounded font-mono text-sm text-gray-500 break-all">
{{ $page.props.jetstream.flash.token }}
<div
v-if="page.props.jetstream.flash.token"
class="mt-4 bg-gray-100 dark:bg-gray-900 px-4 py-2 rounded font-mono text-sm text-gray-500 break-all">
{{ page.props.jetstream.flash.token }}
</div>
</template>
@@ -194,17 +232,24 @@ const deleteApiToken = () => {
</DialogModal>
<!-- API Token Permissions Modal -->
<DialogModal :show="managingPermissionsFor != null" @close="managingPermissionsFor = null">
<template #title>
API Token Permissions
</template>
<DialogModal
:show="managingPermissionsFor != null"
@close="managingPermissionsFor = null">
<template #title> API Token Permissions </template>
<template #content>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div v-for="permission in availablePermissions" :key="permission">
<div
v-for="permission in availablePermissions"
:key="permission">
<label class="flex items-center">
<Checkbox v-model:checked="updateApiTokenForm.permissions" :value="permission" />
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400">{{ permission }}</span>
<Checkbox
v-model:checked="updateApiTokenForm.permissions"
:value="permission" />
<span
class="ms-2 text-sm text-gray-600 dark:text-gray-400"
>{{ permission }}</span
>
</label>
</div>
</div>
@@ -219,18 +264,17 @@ const deleteApiToken = () => {
class="ms-3"
:class="{ 'opacity-25': updateApiTokenForm.processing }"
:disabled="updateApiTokenForm.processing"
@click="updateApiToken"
>
@click="updateApiToken">
Save
</PrimaryButton>
</template>
</DialogModal>
<!-- Delete Token Confirmation Modal -->
<ConfirmationModal :show="apiTokenBeingDeleted != null" @close="apiTokenBeingDeleted = null">
<template #title>
Delete API Token
</template>
<ConfirmationModal
:show="apiTokenBeingDeleted != null"
@close="apiTokenBeingDeleted = null">
<template #title> Delete API Token </template>
<template #content>
Are you sure you would like to delete this API token?
@@ -245,8 +289,7 @@ const deleteApiToken = () => {
class="ms-3"
:class="{ 'opacity-25': deleteApiTokenForm.processing }"
:disabled="deleteApiTokenForm.processing"
@click="deleteApiToken"
>
@click="deleteApiToken">
Delete
</DangerButton>
</template>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { ref } from 'vue';
import { Head, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
@@ -12,14 +12,14 @@ const form = useForm({
password: '',
});
const passwordInput = ref(null);
const passwordInput = ref<HTMLInputElement | null>(null);
const submit = () => {
form.post(route('password.confirm'), {
onFinish: () => {
form.reset();
passwordInput.value.focus();
passwordInput.value?.focus();
},
});
};
@@ -34,7 +34,8 @@ const submit = () => {
</template>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
This is a secure area of the application. Please confirm your password before continuing.
This is a secure area of the application. Please confirm your
password before continuing.
</div>
<form @submit.prevent="submit">
@@ -48,13 +49,15 @@ const submit = () => {
class="mt-1 block w-full"
required
autocomplete="current-password"
autofocus
/>
autofocus />
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="flex justify-end mt-4">
<PrimaryButton class="ms-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
<PrimaryButton
class="ms-4"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing">
Confirm
</PrimaryButton>
</div>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
@@ -29,10 +29,14 @@ const submit = () => {
</template>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.
Forgot your password? No problem. Just let us know your email
address and we will email you a password reset link that will allow
you to choose a new one.
</div>
<div v-if="status" class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
<div
v-if="status"
class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
{{ status }}
</div>
@@ -46,13 +50,14 @@ const submit = () => {
class="mt-1 block w-full"
required
autofocus
autocomplete="username"
/>
autocomplete="username" />
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div class="flex items-center justify-end mt-4">
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
<PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing">
Email Password Reset Link
</PrimaryButton>
</div>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { Head, Link, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
@@ -7,6 +7,7 @@ import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import route from 'ziggy-js';
defineProps({
canResetPassword: Boolean,
@@ -20,7 +21,7 @@ const form = useForm({
});
const submit = () => {
form.transform(data => ({
form.transform((data) => ({
...data,
remember: form.remember ? 'on' : '',
})).post(route('login'), {
@@ -37,7 +38,9 @@ const submit = () => {
<AuthenticationCardLogo />
</template>
<div v-if="status" class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
<div
v-if="status"
class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
{{ status }}
</div>
@@ -51,8 +54,7 @@ const submit = () => {
class="mt-1 block w-full"
required
autofocus
autocomplete="username"
/>
autocomplete="username" />
<InputError class="mt-2" :message="form.errors.email" />
</div>
@@ -64,24 +66,31 @@ const submit = () => {
type="password"
class="mt-1 block w-full"
required
autocomplete="current-password"
/>
autocomplete="current-password" />
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="block mt-4">
<label class="flex items-center">
<Checkbox v-model:checked="form.remember" name="remember" />
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400">Remember me</span>
<span class="ms-2 text-sm text-gray-600 dark:text-gray-400"
>Remember me</span
>
</label>
</div>
<div class="flex items-center justify-end mt-4">
<Link v-if="canResetPassword" :href="route('password.request')" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
<Link
v-if="canResetPassword"
:href="route('password.request')"
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
Forgot your password?
</Link>
<PrimaryButton class="ms-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
<PrimaryButton
class="ms-4"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing">
Log in
</PrimaryButton>
</div>

View File

@@ -1,5 +1,5 @@
<script setup>
import { Head, Link, useForm } from '@inertiajs/vue3';
<script setup lang="ts">
import { Head, Link, useForm, usePage } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import Checkbox from '@/Components/Checkbox.vue';
@@ -7,6 +7,7 @@ import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import route from 'ziggy-js';
const form = useForm({
name: '',
@@ -21,6 +22,12 @@ const submit = () => {
onFinish: () => form.reset('password', 'password_confirmation'),
});
};
const page = usePage<{
jetstream: {
hasTermsAndPrivacyPolicyFeature: boolean;
};
}>();
</script>
<template>
@@ -41,8 +48,7 @@ const submit = () => {
class="mt-1 block w-full"
required
autofocus
autocomplete="name"
/>
autocomplete="name" />
<InputError class="mt-2" :message="form.errors.name" />
</div>
@@ -54,8 +60,7 @@ const submit = () => {
type="email"
class="mt-1 block w-full"
required
autocomplete="username"
/>
autocomplete="username" />
<InputError class="mt-2" :message="form.errors.email" />
</div>
@@ -67,31 +72,52 @@ const submit = () => {
type="password"
class="mt-1 block w-full"
required
autocomplete="new-password"
/>
autocomplete="new-password" />
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="mt-4">
<InputLabel for="password_confirmation" value="Confirm Password" />
<InputLabel
for="password_confirmation"
value="Confirm Password" />
<TextInput
id="password_confirmation"
v-model="form.password_confirmation"
type="password"
class="mt-1 block w-full"
required
autocomplete="new-password"
/>
<InputError class="mt-2" :message="form.errors.password_confirmation" />
autocomplete="new-password" />
<InputError
class="mt-2"
:message="form.errors.password_confirmation" />
</div>
<div v-if="$page.props.jetstream.hasTermsAndPrivacyPolicyFeature" class="mt-4">
<div
v-if="page.props.jetstream.hasTermsAndPrivacyPolicyFeature"
class="mt-4">
<InputLabel for="terms">
<div class="flex items-center">
<Checkbox id="terms" v-model:checked="form.terms" name="terms" required />
<Checkbox
id="terms"
v-model:checked="form.terms"
name="terms"
required />
<div class="ms-2">
I agree to the <a target="_blank" :href="route('terms.show')" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">Terms of Service</a> and <a target="_blank" :href="route('policy.show')" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">Privacy Policy</a>
I agree to the
<a
target="_blank"
:href="route('terms.show')"
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
>Terms of Service</a
>
and
<a
target="_blank"
:href="route('policy.show')"
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
>Privacy Policy</a
>
</div>
</div>
<InputError class="mt-2" :message="form.errors.terms" />
@@ -99,11 +125,16 @@ const submit = () => {
</div>
<div class="flex items-center justify-end mt-4">
<Link :href="route('login')" class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
<Link
:href="route('login')"
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
Already registered?
</Link>
<PrimaryButton class="ms-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
<PrimaryButton
class="ms-4"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing">
Register
</PrimaryButton>
</div>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { Head, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
@@ -44,8 +44,7 @@ const submit = () => {
class="mt-1 block w-full"
required
autofocus
autocomplete="username"
/>
autocomplete="username" />
<InputError class="mt-2" :message="form.errors.email" />
</div>
@@ -57,26 +56,30 @@ const submit = () => {
type="password"
class="mt-1 block w-full"
required
autocomplete="new-password"
/>
autocomplete="new-password" />
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="mt-4">
<InputLabel for="password_confirmation" value="Confirm Password" />
<InputLabel
for="password_confirmation"
value="Confirm Password" />
<TextInput
id="password_confirmation"
v-model="form.password_confirmation"
type="password"
class="mt-1 block w-full"
required
autocomplete="new-password"
/>
<InputError class="mt-2" :message="form.errors.password_confirmation" />
autocomplete="new-password" />
<InputError
class="mt-2"
:message="form.errors.password_confirmation" />
</div>
<div class="flex items-center justify-end mt-4">
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
<PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing">
Reset Password
</PrimaryButton>
</div>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { nextTick, ref } from 'vue';
import { Head, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
@@ -15,19 +15,19 @@ const form = useForm({
recovery_code: '',
});
const recoveryCodeInput = ref(null);
const codeInput = ref(null);
const recoveryCodeInput = ref<HTMLInputElement | null>(null);
const codeInput = ref<HTMLInputElement | null>(null);
const toggleRecovery = async () => {
recovery.value ^= true;
recovery.value = !recovery.value;
await nextTick();
if (recovery.value) {
recoveryCodeInput.value.focus();
recoveryCodeInput.value?.focus();
form.code = '';
} else {
codeInput.value.focus();
codeInput.value?.focus();
form.recovery_code = '';
}
};
@@ -46,17 +46,19 @@ const submit = () => {
</template>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
<template v-if="! recovery">
Please confirm access to your account by entering the authentication code provided by your authenticator application.
<template v-if="!recovery">
Please confirm access to your account by entering the
authentication code provided by your authenticator application.
</template>
<template v-else>
Please confirm access to your account by entering one of your emergency recovery codes.
Please confirm access to your account by entering one of your
emergency recovery codes.
</template>
</div>
<form @submit.prevent="submit">
<div v-if="! recovery">
<div v-if="!recovery">
<InputLabel for="code" value="Code" />
<TextInput
id="code"
@@ -66,8 +68,7 @@ const submit = () => {
inputmode="numeric"
class="mt-1 block w-full"
autofocus
autocomplete="one-time-code"
/>
autocomplete="one-time-code" />
<InputError class="mt-2" :message="form.errors.code" />
</div>
@@ -79,23 +80,24 @@ const submit = () => {
v-model="form.recovery_code"
type="text"
class="mt-1 block w-full"
autocomplete="one-time-code"
/>
autocomplete="one-time-code" />
<InputError class="mt-2" :message="form.errors.recovery_code" />
</div>
<div class="flex items-center justify-end mt-4">
<button type="button" class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 underline cursor-pointer" @click.prevent="toggleRecovery">
<template v-if="! recovery">
Use a recovery code
</template>
<button
type="button"
class="text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 underline cursor-pointer"
@click.prevent="toggleRecovery">
<template v-if="!recovery"> Use a recovery code</template>
<template v-else>
Use an authentication code
</template>
<template v-else> Use an authentication code</template>
</button>
<PrimaryButton class="ms-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
<PrimaryButton
class="ms-4"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing">
Log in
</PrimaryButton>
</div>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { computed } from 'vue';
import { Head, Link, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
@@ -8,14 +8,16 @@ import PrimaryButton from '@/Components/PrimaryButton.vue';
const props = defineProps({
status: String,
});
import route from 'ziggy-js';
const form = useForm({});
const submit = () => {
form.post(route('verification.send'));
};
const verificationLinkSent = computed(() => props.status === 'verification-link-sent');
const verificationLinkSent = computed(
() => props.status === 'verification-link-sent'
);
</script>
<template>
@@ -27,32 +29,38 @@ const verificationLinkSent = computed(() => props.status === 'verification-link-
</template>
<div class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Before continuing, could you verify your email address by clicking on the link we just emailed to you? If you didn't receive the email, we will gladly send you another.
Before continuing, could you verify your email address by clicking
on the link we just emailed to you? If you didn't receive the email,
we will gladly send you another.
</div>
<div v-if="verificationLinkSent" class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
A new verification link has been sent to the email address you provided in your profile settings.
<div
v-if="verificationLinkSent"
class="mb-4 font-medium text-sm text-green-600 dark:text-green-400">
A new verification link has been sent to the email address you
provided in your profile settings.
</div>
<form @submit.prevent="submit">
<div class="mt-4 flex items-center justify-between">
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
<PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing">
Resend Verification Email
</PrimaryButton>
<div>
<Link
:href="route('profile.show')"
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800">
Edit Profile</Link
>
Edit Profile</Link>
<Link
:href="route('logout')"
method="post"
as="button"
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800 ms-2"
>
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800 ms-2">
Log Out
</Link>
</div>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import AppLayout from '@/Layouts/AppLayout.vue';
import Welcome from '@/Components/Welcome.vue';
</script>
@@ -6,14 +6,16 @@ import Welcome from '@/Components/Welcome.vue';
<template>
<AppLayout title="Dashboard">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
<h2
class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
Dashboard
</h2>
</template>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">
<div
class="bg-white dark:bg-gray-800 overflow-hidden shadow-xl sm:rounded-lg">
<Welcome />
</div>
</div>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
@@ -17,7 +17,9 @@ defineProps({
<AuthenticationCardLogo />
</div>
<div class="w-full sm:max-w-2xl mt-6 p-6 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg prose dark:prose-invert" v-html="policy" />
<div
class="w-full sm:max-w-2xl mt-6 p-6 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg prose dark:prose-invert"
v-html="policy" />
</div>
</div>
</div>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3';
import ActionSection from '@/Components/ActionSection.vue';
@@ -9,7 +9,7 @@ import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
const confirmingUserDeletion = ref(false);
const passwordInput = ref(null);
const passwordInput = ref<HTMLElement | null>(null);
const form = useForm({
password: '',
@@ -18,14 +18,14 @@ const form = useForm({
const confirmUserDeletion = () => {
confirmingUserDeletion.value = true;
setTimeout(() => passwordInput.value.focus(), 250);
setTimeout(() => passwordInput.value?.focus(), 250);
};
const deleteUser = () => {
form.delete(route('current-user.destroy'), {
preserveScroll: true,
onSuccess: () => closeModal(),
onError: () => passwordInput.value.focus(),
onError: () => passwordInput.value?.focus(),
onFinish: () => form.reset(),
});
};
@@ -39,17 +39,15 @@ const closeModal = () => {
<template>
<ActionSection>
<template #title>
Delete Account
</template>
<template #title> Delete Account </template>
<template #description>
Permanently delete your account.
</template>
<template #description> Permanently delete your account. </template>
<template #content>
<div class="max-w-xl text-sm text-gray-600 dark:text-gray-400">
Once your account is deleted, all of its resources and data will be permanently deleted. Before deleting your account, please download any data or information that you wish to retain.
Once your account is deleted, all of its resources and data will
be permanently deleted. Before deleting your account, please
download any data or information that you wish to retain.
</div>
<div class="mt-5">
@@ -60,12 +58,13 @@ const closeModal = () => {
<!-- Delete Account Confirmation Modal -->
<DialogModal :show="confirmingUserDeletion" @close="closeModal">
<template #title>
Delete Account
</template>
<template #title> Delete Account </template>
<template #content>
Are you sure you want to delete your account? Once your account is deleted, all of its resources and data will be permanently deleted. Please enter your password to confirm you would like to permanently delete your account.
Are you sure you want to delete your account? Once your
account is deleted, all of its resources and data will be
permanently deleted. Please enter your password to confirm
you would like to permanently delete your account.
<div class="mt-4">
<TextInput
@@ -75,10 +74,11 @@ const closeModal = () => {
class="mt-1 block w-3/4"
placeholder="Password"
autocomplete="current-password"
@keyup.enter="deleteUser"
/>
@keyup.enter="deleteUser" />
<InputError :message="form.errors.password" class="mt-2" />
<InputError
:message="form.errors.password"
class="mt-2" />
</div>
</template>
@@ -91,8 +91,7 @@ const closeModal = () => {
class="ms-3"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="deleteUser"
>
@click="deleteUser">
Delete Account
</DangerButton>
</template>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
@@ -8,13 +8,14 @@ import InputError from '@/Components/InputError.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import type { Session } from '@/types/jetstream';
defineProps({
sessions: Array,
});
defineProps<{
sessions: Session[];
}>();
const confirmingLogout = ref(false);
const passwordInput = ref(null);
const passwordInput = ref<HTMLElement | null>(null);
const form = useForm({
password: '',
@@ -23,14 +24,14 @@ const form = useForm({
const confirmLogout = () => {
confirmingLogout.value = true;
setTimeout(() => passwordInput.value.focus(), 250);
setTimeout(() => passwordInput.value?.focus(), 250);
};
const logoutOtherBrowserSessions = () => {
form.delete(route('other-browser-sessions.destroy'), {
preserveScroll: true,
onSuccess: () => closeModal(),
onError: () => passwordInput.value.focus(),
onError: () => passwordInput.value?.focus(),
onFinish: () => form.reset(),
});
};
@@ -44,43 +45,85 @@ const closeModal = () => {
<template>
<ActionSection>
<template #title>
Browser Sessions
</template>
<template #title> Browser Sessions </template>
<template #description>
Manage and log out your active sessions on other browsers and devices.
Manage and log out your active sessions on other browsers and
devices.
</template>
<template #content>
<div class="max-w-xl text-sm text-gray-600 dark:text-gray-400">
If necessary, you may log out of all of your other browser sessions across all of your devices. Some of your recent sessions are listed below; however, this list may not be exhaustive. If you feel your account has been compromised, you should also update your password.
If necessary, you may log out of all of your other browser
sessions across all of your devices. Some of your recent
sessions are listed below; however, this list may not be
exhaustive. If you feel your account has been compromised, you
should also update your password.
</div>
<!-- Other Browser Sessions -->
<div v-if="sessions.length > 0" class="mt-5 space-y-6">
<div v-for="(session, i) in sessions" :key="i" class="flex items-center">
<div
v-for="(session, i) in sessions"
:key="i"
class="flex items-center">
<div>
<svg v-if="session.agent.is_desktop" class="w-8 h-8 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
<svg
v-if="session.agent.is_desktop"
class="w-8 h-8 text-gray-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25" />
</svg>
<svg v-else class="w-8 h-8 text-gray-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
<svg
v-else
class="w-8 h-8 text-gray-500"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M10.5 1.5H8.25A2.25 2.25 0 006 3.75v16.5a2.25 2.25 0 002.25 2.25h7.5A2.25 2.25 0 0018 20.25V3.75a2.25 2.25 0 00-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
</svg>
</div>
<div class="ms-3">
<div class="text-sm text-gray-600 dark:text-gray-400">
{{ session.agent.platform ? session.agent.platform : 'Unknown' }} - {{ session.agent.browser ? session.agent.browser : 'Unknown' }}
{{
session.agent.platform
? session.agent.platform
: 'Unknown'
}}
-
{{
session.agent.browser
? session.agent.browser
: 'Unknown'
}}
</div>
<div>
<div class="text-xs text-gray-500">
{{ session.ip_address }},
<span v-if="session.is_current_device" class="text-green-500 font-semibold">This device</span>
<span v-else>Last active {{ session.last_active }}</span>
<span
v-if="session.is_current_device"
class="text-green-500 font-semibold"
>This device</span
>
<span v-else
>Last active {{ session.last_active }}</span
>
</div>
</div>
</div>
@@ -99,12 +142,12 @@ const closeModal = () => {
<!-- Log Out Other Devices Confirmation Modal -->
<DialogModal :show="confirmingLogout" @close="closeModal">
<template #title>
Log Out Other Browser Sessions
</template>
<template #title> Log Out Other Browser Sessions </template>
<template #content>
Please enter your password to confirm you would like to log out of your other browser sessions across all of your devices.
Please enter your password to confirm you would like to log
out of your other browser sessions across all of your
devices.
<div class="mt-4">
<TextInput
@@ -114,10 +157,11 @@ const closeModal = () => {
class="mt-1 block w-3/4"
placeholder="Password"
autocomplete="current-password"
@keyup.enter="logoutOtherBrowserSessions"
/>
@keyup.enter="logoutOtherBrowserSessions" />
<InputError :message="form.errors.password" class="mt-2" />
<InputError
:message="form.errors.password"
class="mt-2" />
</div>
</template>
@@ -130,8 +174,7 @@ const closeModal = () => {
class="ms-3"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="logoutOtherBrowserSessions"
>
@click="logoutOtherBrowserSessions">
Log Out Other Browser Sessions
</PrimaryButton>
</template>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { ref, computed, watch } from 'vue';
import { router, useForm, usePage } from '@inertiajs/vue3';
import ActionSection from '@/Components/ActionSection.vue';
@@ -9,12 +9,18 @@ import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import axios from 'axios';
import type { JetstreamUser } from '@/types/jetstream';
const props = defineProps({
requiresConfirmation: Boolean,
});
const props = defineProps<{
requiresConfirmation: boolean;
}>();
const page = usePage();
const page = usePage<{
auth: {
user: JetstreamUser;
};
}>();
const enabling = ref(false);
const confirming = ref(false);
const disabling = ref(false);
@@ -27,11 +33,11 @@ const confirmationForm = useForm({
});
const twoFactorEnabled = computed(
() => ! enabling.value && page.props.auth.user?.two_factor_enabled,
() => !enabling.value && page.props.auth.user?.two_factor_enabled
);
watch(twoFactorEnabled, () => {
if (! twoFactorEnabled.value) {
if (!twoFactorEnabled.value) {
confirmationForm.reset();
confirmationForm.clearErrors();
}
@@ -40,41 +46,46 @@ watch(twoFactorEnabled, () => {
const enableTwoFactorAuthentication = () => {
enabling.value = true;
router.post(route('two-factor.enable'), {}, {
preserveScroll: true,
onSuccess: () => Promise.all([
showQrCode(),
showSetupKey(),
showRecoveryCodes(),
]),
onFinish: () => {
enabling.value = false;
confirming.value = props.requiresConfirmation;
},
});
router.post(
route('two-factor.enable'),
{},
{
preserveScroll: true,
onSuccess: () =>
Promise.all([
showQrCode(),
showSetupKey(),
showRecoveryCodes(),
]),
onFinish: () => {
enabling.value = false;
confirming.value = props.requiresConfirmation;
},
}
);
};
const showQrCode = () => {
return axios.get(route('two-factor.qr-code')).then(response => {
return axios.get(route('two-factor.qr-code')).then((response) => {
qrCode.value = response.data.svg;
});
};
const showSetupKey = () => {
return axios.get(route('two-factor.secret-key')).then(response => {
return axios.get(route('two-factor.secret-key')).then((response) => {
setupKey.value = response.data.secretKey;
});
}
};
const showRecoveryCodes = () => {
return axios.get(route('two-factor.recovery-codes')).then(response => {
return axios.get(route('two-factor.recovery-codes')).then((response) => {
recoveryCodes.value = response.data;
});
};
const confirmTwoFactorAuthentication = () => {
confirmationForm.post(route('two-factor.confirm'), {
errorBag: "confirmTwoFactorAuthentication",
errorBag: 'confirmTwoFactorAuthentication',
preserveScroll: true,
preserveState: true,
onSuccess: () => {
@@ -106,48 +117,66 @@ const disableTwoFactorAuthentication = () => {
<template>
<ActionSection>
<template #title>
Two Factor Authentication
</template>
<template #title> Two Factor Authentication </template>
<template #description>
Add additional security to your account using two factor authentication.
Add additional security to your account using two factor
authentication.
</template>
<template #content>
<h3 v-if="twoFactorEnabled && ! confirming" class="text-lg font-medium text-gray-900 dark:text-gray-100">
<h3
v-if="twoFactorEnabled && !confirming"
class="text-lg font-medium text-gray-900 dark:text-gray-100">
You have enabled two factor authentication.
</h3>
<h3 v-else-if="twoFactorEnabled && confirming" class="text-lg font-medium text-gray-900 dark:text-gray-100">
<h3
v-else-if="twoFactorEnabled && confirming"
class="text-lg font-medium text-gray-900 dark:text-gray-100">
Finish enabling two factor authentication.
</h3>
<h3 v-else class="text-lg font-medium text-gray-900 dark:text-gray-100">
<h3
v-else
class="text-lg font-medium text-gray-900 dark:text-gray-100">
You have not enabled two factor authentication.
</h3>
<div class="mt-3 max-w-xl text-sm text-gray-600 dark:text-gray-400">
<p>
When two factor authentication is enabled, you will be prompted for a secure, random token during authentication. You may retrieve this token from your phone's Google Authenticator application.
When two factor authentication is enabled, you will be
prompted for a secure, random token during authentication.
You may retrieve this token from your phone's Google
Authenticator application.
</p>
</div>
<div v-if="twoFactorEnabled">
<div v-if="qrCode">
<div class="mt-4 max-w-xl text-sm text-gray-600 dark:text-gray-400">
<div
class="mt-4 max-w-xl text-sm text-gray-600 dark:text-gray-400">
<p v-if="confirming" class="font-semibold">
To finish enabling two factor authentication, scan the following QR code using your phone's authenticator application or enter the setup key and provide the generated OTP code.
To finish enabling two factor authentication, scan
the following QR code using your phone's
authenticator application or enter the setup key and
provide the generated OTP code.
</p>
<p v-else>
Two factor authentication is now enabled. Scan the following QR code using your phone's authenticator application or enter the setup key.
Two factor authentication is now enabled. Scan the
following QR code using your phone's authenticator
application or enter the setup key.
</p>
</div>
<div class="mt-4 p-2 inline-block bg-white" v-html="qrCode" />
<div
class="mt-4 p-2 inline-block bg-white"
v-html="qrCode" />
<div v-if="setupKey" class="mt-4 max-w-xl text-sm text-gray-600 dark:text-gray-400">
<div
v-if="setupKey"
class="mt-4 max-w-xl text-sm text-gray-600 dark:text-gray-400">
<p class="font-semibold">
Setup Key: <span v-html="setupKey"></span>
</p>
@@ -165,21 +194,27 @@ const disableTwoFactorAuthentication = () => {
inputmode="numeric"
autofocus
autocomplete="one-time-code"
@keyup.enter="confirmTwoFactorAuthentication"
/>
@keyup.enter="confirmTwoFactorAuthentication" />
<InputError :message="confirmationForm.errors.code" class="mt-2" />
<InputError
:message="confirmationForm.errors.code"
class="mt-2" />
</div>
</div>
<div v-if="recoveryCodes.length > 0 && ! confirming">
<div class="mt-4 max-w-xl text-sm text-gray-600 dark:text-gray-400">
<div v-if="recoveryCodes.length > 0 && !confirming">
<div
class="mt-4 max-w-xl text-sm text-gray-600 dark:text-gray-400">
<p class="font-semibold">
Store these recovery codes in a secure password manager. They can be used to recover access to your account if your two factor authentication device is lost.
Store these recovery codes in a secure password
manager. They can be used to recover access to your
account if your two factor authentication device is
lost.
</p>
</div>
<div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 dark:bg-gray-900 dark:text-gray-100 rounded-lg">
<div
class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 dark:bg-gray-900 dark:text-gray-100 rounded-lg">
<div v-for="code in recoveryCodes" :key="code">
{{ code }}
</div>
@@ -188,61 +223,63 @@ const disableTwoFactorAuthentication = () => {
</div>
<div class="mt-5">
<div v-if="! twoFactorEnabled">
<ConfirmsPassword @confirmed="enableTwoFactorAuthentication">
<PrimaryButton type="button" :class="{ 'opacity-25': enabling }" :disabled="enabling">
<div v-if="!twoFactorEnabled">
<ConfirmsPassword
@confirmed="enableTwoFactorAuthentication">
<PrimaryButton
type="button"
:class="{ 'opacity-25': enabling }"
:disabled="enabling">
Enable
</PrimaryButton>
</ConfirmsPassword>
</div>
<div v-else>
<ConfirmsPassword @confirmed="confirmTwoFactorAuthentication">
<ConfirmsPassword
@confirmed="confirmTwoFactorAuthentication">
<PrimaryButton
v-if="confirming"
type="button"
class="me-3"
:class="{ 'opacity-25': enabling }"
:disabled="enabling"
>
:disabled="enabling">
Confirm
</PrimaryButton>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="regenerateRecoveryCodes">
<SecondaryButton
v-if="recoveryCodes.length > 0 && ! confirming"
class="me-3"
>
v-if="recoveryCodes.length > 0 && !confirming"
class="me-3">
Regenerate Recovery Codes
</SecondaryButton>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="showRecoveryCodes">
<SecondaryButton
v-if="recoveryCodes.length === 0 && ! confirming"
class="me-3"
>
v-if="recoveryCodes.length === 0 && !confirming"
class="me-3">
Show Recovery Codes
</SecondaryButton>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<ConfirmsPassword
@confirmed="disableTwoFactorAuthentication">
<SecondaryButton
v-if="confirming"
:class="{ 'opacity-25': disabling }"
:disabled="disabling"
>
:disabled="disabling">
Cancel
</SecondaryButton>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<ConfirmsPassword
@confirmed="disableTwoFactorAuthentication">
<DangerButton
v-if="! confirming"
v-if="!confirming"
:class="{ 'opacity-25': disabling }"
:disabled="disabling"
>
:disabled="disabling">
Disable
</DangerButton>
</ConfirmsPassword>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
@@ -8,8 +8,8 @@ import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
const passwordInput = ref(null);
const currentPasswordInput = ref(null);
const passwordInput = ref<HTMLElement | null>(null);
const currentPasswordInput = ref<HTMLElement | null>(null);
const form = useForm({
current_password: '',
@@ -25,12 +25,12 @@ const updatePassword = () => {
onError: () => {
if (form.errors.password) {
form.reset('password', 'password_confirmation');
passwordInput.value.focus();
passwordInput.value?.focus();
}
if (form.errors.current_password) {
form.reset('current_password');
currentPasswordInput.value.focus();
currentPasswordInput.value?.focus();
}
},
});
@@ -39,9 +39,7 @@ const updatePassword = () => {
<template>
<FormSection @submitted="updatePassword">
<template #title>
Update Password
</template>
<template #title> Update Password </template>
<template #description>
Ensure your account is using a long, random password to stay secure.
@@ -56,9 +54,10 @@ const updatePassword = () => {
v-model="form.current_password"
type="password"
class="mt-1 block w-full"
autocomplete="current-password"
/>
<InputError :message="form.errors.current_password" class="mt-2" />
autocomplete="current-password" />
<InputError
:message="form.errors.current_password"
class="mt-2" />
</div>
<div class="col-span-6 sm:col-span-4">
@@ -69,21 +68,23 @@ const updatePassword = () => {
v-model="form.password"
type="password"
class="mt-1 block w-full"
autocomplete="new-password"
/>
autocomplete="new-password" />
<InputError :message="form.errors.password" class="mt-2" />
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="password_confirmation" value="Confirm Password" />
<InputLabel
for="password_confirmation"
value="Confirm Password" />
<TextInput
id="password_confirmation"
v-model="form.password_confirmation"
type="password"
class="mt-1 block w-full"
autocomplete="new-password"
/>
<InputError :message="form.errors.password_confirmation" class="mt-2" />
autocomplete="new-password" />
<InputError
:message="form.errors.password_confirmation"
class="mt-2" />
</div>
</template>
@@ -92,7 +93,9 @@ const updatePassword = () => {
Saved.
</ActionMessage>
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
<PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing">
Save
</PrimaryButton>
</template>

View File

@@ -1,6 +1,6 @@
<script setup>
<script setup lang="ts">
import { ref } from 'vue';
import { Link, router, useForm } from '@inertiajs/vue3';
import { Link, router, useForm, usePage } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import FormSection from '@/Components/FormSection.vue';
import InputError from '@/Components/InputError.vue';
@@ -8,25 +8,31 @@ import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import type { User } from '@/types/models';
import route from 'ziggy-js';
const props = defineProps({
user: Object,
});
const props = defineProps<{
user: User;
}>();
const form = useForm({
_method: 'PUT',
name: props.user.name,
email: props.user.email,
photo: null,
photo: null as File | null,
});
const verificationLinkSent = ref(null);
const photoPreview = ref(null);
const photoInput = ref(null);
const verificationLinkSent = ref<boolean | null>(null);
const photoPreview = ref<ArrayBuffer | undefined | string | null>(null);
const photoInput = ref<HTMLInputElement | null>(null);
const updateProfileInformation = () => {
if (photoInput.value) {
form.photo = photoInput.value.files[0];
if (
photoInput.value &&
photoInput.value.files &&
photoInput.value.files?.length > 0
) {
form.photo = photoInput.value?.files[0];
}
form.post(route('user-profile-information.update'), {
@@ -41,21 +47,22 @@ const sendEmailVerification = () => {
};
const selectNewPhoto = () => {
photoInput.value.click();
photoInput.value?.click();
};
const updatePhotoPreview = () => {
const photo = photoInput.value.files[0];
if (photoInput.value?.files) {
const photo = photoInput.value?.files[0];
if (!photo) return;
if (! photo) return;
const reader = new FileReader();
const reader = new FileReader();
reader.onload = (e) => {
photoPreview.value = e.target?.result;
};
reader.onload = (e) => {
photoPreview.value = e.target.result;
};
reader.readAsDataURL(photo);
reader.readAsDataURL(photo);
}
};
const deletePhoto = () => {
@@ -70,16 +77,21 @@ const deletePhoto = () => {
const clearPhotoFileInput = () => {
if (photoInput.value?.value) {
photoInput.value.value = null;
photoInput.value.value = '';
}
};
const page = usePage<{
jetstream: {
managesProfilePhotos: boolean;
hasEmailVerification: boolean;
};
}>();
</script>
<template>
<FormSection @submitted="updateProfileInformation">
<template #title>
Profile Information
</template>
<template #title> Profile Information</template>
<template #description>
Update your account's profile information and email address.
@@ -87,32 +99,40 @@ const clearPhotoFileInput = () => {
<template #form>
<!-- Profile Photo -->
<div v-if="$page.props.jetstream.managesProfilePhotos" class="col-span-6 sm:col-span-4">
<div
v-if="page.props.jetstream.managesProfilePhotos"
class="col-span-6 sm:col-span-4">
<!-- Profile Photo File Input -->
<input
id="photo"
ref="photoInput"
type="file"
class="hidden"
@change="updatePhotoPreview"
>
@change="updatePhotoPreview" />
<InputLabel for="photo" value="Photo" />
<!-- Current Profile Photo -->
<div v-show="! photoPreview" class="mt-2">
<img :src="user.profile_photo_url" :alt="user.name" class="rounded-full h-20 w-20 object-cover">
<div v-show="!photoPreview" class="mt-2">
<img
:src="user.profile_photo_url"
:alt="user.name"
class="rounded-full h-20 w-20 object-cover" />
</div>
<!-- New Profile Photo Preview -->
<div v-show="photoPreview" class="mt-2">
<span
class="block rounded-full w-20 h-20 bg-cover bg-no-repeat bg-center"
:style="'background-image: url(\'' + photoPreview + '\');'"
/>
:style="
'background-image: url(\'' + photoPreview + '\');'
" />
</div>
<SecondaryButton class="mt-2 me-2" type="button" @click.prevent="selectNewPhoto">
<SecondaryButton
class="mt-2 me-2"
type="button"
@click.prevent="selectNewPhoto">
Select A New Photo
</SecondaryButton>
@@ -120,8 +140,7 @@ const clearPhotoFileInput = () => {
v-if="user.profile_photo_path"
type="button"
class="mt-2"
@click.prevent="deletePhoto"
>
@click.prevent="deletePhoto">
Remove Photo
</SecondaryButton>
@@ -137,8 +156,7 @@ const clearPhotoFileInput = () => {
type="text"
class="mt-1 block w-full"
required
autocomplete="name"
/>
autocomplete="name" />
<InputError :message="form.errors.name" class="mt-2" />
</div>
@@ -151,11 +169,14 @@ const clearPhotoFileInput = () => {
type="email"
class="mt-1 block w-full"
required
autocomplete="username"
/>
autocomplete="username" />
<InputError :message="form.errors.email" class="mt-2" />
<div v-if="$page.props.jetstream.hasEmailVerification && user.email_verified_at === null">
<div
v-if="
page.props.jetstream.hasEmailVerification &&
user.email_verified_at === null
">
<p class="text-sm mt-2 dark:text-white">
Your email address is unverified.
@@ -164,14 +185,16 @@ const clearPhotoFileInput = () => {
method="post"
as="button"
class="underline text-sm text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 dark:focus:ring-offset-gray-800"
@click.prevent="sendEmailVerification"
>
@click.prevent="sendEmailVerification">
Click here to re-send the verification email.
</Link>
</p>
<div v-show="verificationLinkSent" class="mt-2 font-medium text-sm text-green-600 dark:text-green-400">
A new verification link has been sent to your email address.
<div
v-show="verificationLinkSent"
class="mt-2 font-medium text-sm text-green-600 dark:text-green-400">
A new verification link has been sent to your email
address.
</div>
</div>
</div>
@@ -182,7 +205,9 @@ const clearPhotoFileInput = () => {
Saved.
</ActionMessage>
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
<PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing">
Save
</PrimaryButton>
</template>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import AppLayout from '@/Layouts/AppLayout.vue';
import DeleteUserForm from '@/Pages/Profile/Partials/DeleteUserForm.vue';
import LogoutOtherBrowserSessionsForm from '@/Pages/Profile/Partials/LogoutOtherBrowserSessionsForm.vue';
@@ -6,47 +6,69 @@ import SectionBorder from '@/Components/SectionBorder.vue';
import TwoFactorAuthenticationForm from '@/Pages/Profile/Partials/TwoFactorAuthenticationForm.vue';
import UpdatePasswordForm from '@/Pages/Profile/Partials/UpdatePasswordForm.vue';
import UpdateProfileInformationForm from '@/Pages/Profile/Partials/UpdateProfileInformationForm.vue';
import { usePage } from '@inertiajs/vue3';
import type { User } from '@/types/models';
import type { Session } from '@/types/jetstream';
defineProps({
confirmsTwoFactorAuthentication: Boolean,
sessions: Array,
});
defineProps<{
confirmsTwoFactorAuthentication: boolean;
sessions: Session[];
}>();
const page = usePage<{
jetstream: {
canUpdateProfileInformation: boolean;
canUpdatePassword: boolean;
canManageTwoFactorAuthentication: boolean;
hasAccountDeletionFeatures: boolean;
};
auth: {
user: User;
};
}>();
</script>
<template>
<AppLayout title="Profile">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
<h2
class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
Profile
</h2>
</template>
<div>
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
<div v-if="$page.props.jetstream.canUpdateProfileInformation">
<UpdateProfileInformationForm :user="$page.props.auth.user" />
<div v-if="page.props.jetstream.canUpdateProfileInformation">
<UpdateProfileInformationForm
:user="page.props.auth.user" />
<SectionBorder />
</div>
<div v-if="$page.props.jetstream.canUpdatePassword">
<div v-if="page.props.jetstream.canUpdatePassword">
<UpdatePasswordForm class="mt-10 sm:mt-0" />
<SectionBorder />
</div>
<div v-if="$page.props.jetstream.canManageTwoFactorAuthentication">
<div
v-if="
page.props.jetstream.canManageTwoFactorAuthentication
">
<TwoFactorAuthenticationForm
:requires-confirmation="confirmsTwoFactorAuthentication"
class="mt-10 sm:mt-0"
/>
class="mt-10 sm:mt-0" />
<SectionBorder />
</div>
<LogoutOtherBrowserSessionsForm :sessions="sessions" class="mt-10 sm:mt-0" />
<LogoutOtherBrowserSessionsForm
:sessions="sessions"
class="mt-10 sm:mt-0" />
<template v-if="$page.props.jetstream.hasAccountDeletionFeatures">
<template
v-if="page.props.jetstream.hasAccountDeletionFeatures">
<SectionBorder />
<DeleteUserForm class="mt-10 sm:mt-0" />

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import AppLayout from '@/Layouts/AppLayout.vue';
import CreateTeamForm from '@/Pages/Teams/Partials/CreateTeamForm.vue';
</script>
@@ -6,7 +6,8 @@ import CreateTeamForm from '@/Pages/Teams/Partials/CreateTeamForm.vue';
<template>
<AppLayout title="Create Team">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
<h2
class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
Create Team
</h2>
</template>

View File

@@ -1,10 +1,11 @@
<script setup>
import { useForm } from '@inertiajs/vue3';
<script setup lang="ts">
import { useForm, usePage } from '@inertiajs/vue3';
import FormSection from '@/Components/FormSection.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import type { User } from '@/types/models';
const form = useForm({
name: '',
@@ -16,13 +17,16 @@ const createTeam = () => {
preserveScroll: true,
});
};
const page = usePage<{
auth: {
user: User;
};
}>();
</script>
<template>
<FormSection @submitted="createTeam">
<template #title>
Team Details
</template>
<template #title> Team Details</template>
<template #description>
Create a new team to collaborate with others on projects.
@@ -33,12 +37,17 @@ const createTeam = () => {
<InputLabel value="Team Owner" />
<div class="flex items-center mt-2">
<img class="object-cover w-12 h-12 rounded-full" :src="$page.props.auth.user.profile_photo_url" :alt="$page.props.auth.user.name">
<img
class="object-cover w-12 h-12 rounded-full"
:src="page.props.auth.user.profile_photo_url"
:alt="page.props.auth.user.name" />
<div class="ms-4 leading-tight">
<div class="text-gray-900 dark:text-white">{{ $page.props.auth.user.name }}</div>
<div class="text-gray-900 dark:text-white">
{{ page.props.auth.user.name }}
</div>
<div class="text-sm text-gray-700 dark:text-gray-300">
{{ $page.props.auth.user.email }}
{{ page.props.auth.user.email }}
</div>
</div>
</div>
@@ -51,14 +60,15 @@ const createTeam = () => {
v-model="form.name"
type="text"
class="block w-full mt-1"
autofocus
/>
autofocus />
<InputError :message="form.errors.name" class="mt-2" />
</div>
</template>
<template #actions>
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
<PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing">
Create
</PrimaryButton>
</template>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { ref } from 'vue';
import { useForm } from '@inertiajs/vue3';
import ActionSection from '@/Components/ActionSection.vue';
@@ -26,17 +26,16 @@ const deleteTeam = () => {
<template>
<ActionSection>
<template #title>
Delete Team
</template>
<template #title> Delete Team </template>
<template #description>
Permanently delete this team.
</template>
<template #description> Permanently delete this team. </template>
<template #content>
<div class="max-w-xl text-sm text-gray-600 dark:text-gray-400">
Once a team is deleted, all of its resources and data will be permanently deleted. Before deleting this team, please download any data or information regarding this team that you wish to retain.
Once a team is deleted, all of its resources and data will be
permanently deleted. Before deleting this team, please download
any data or information regarding this team that you wish to
retain.
</div>
<div class="mt-5">
@@ -46,13 +45,15 @@ const deleteTeam = () => {
</div>
<!-- Delete Organization Confirmation Modal -->
<ConfirmationModal :show="confirmingTeamDeletion" @close="confirmingTeamDeletion = false">
<template #title>
Delete Team
</template>
<ConfirmationModal
:show="confirmingTeamDeletion"
@close="confirmingTeamDeletion = false">
<template #title> Delete Team </template>
<template #content>
Are you sure you want to delete this team? Once a team is deleted, all of its resources and data will be permanently deleted.
Are you sure you want to delete this team? Once a team is
deleted, all of its resources and data will be permanently
deleted.
</template>
<template #footer>
@@ -64,8 +65,7 @@ const deleteTeam = () => {
class="ms-3"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="deleteTeam"
>
@click="deleteTeam">
Delete Team
</DangerButton>
</template>

View File

@@ -1,5 +1,5 @@
<script setup>
import { ref } from 'vue';
<script setup lang="ts">
import { computed, ref } from 'vue';
import { router, useForm, usePage } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import ActionSection from '@/Components/ActionSection.vue';
@@ -13,57 +13,79 @@ import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import SectionBorder from '@/Components/SectionBorder.vue';
import TextInput from '@/Components/TextInput.vue';
import type {
Organization,
OrganizationInvitation,
User,
} from '@/types/models';
import type { Membership, Permissions, Role } from '@/types/jetstream';
const props = defineProps({
team: Object,
availableRoles: Array,
userPermissions: Object,
type UserWithMembership = User & { membership: Membership };
const props = defineProps<{
team: Organization;
availableRoles: Role[];
userPermissions: Permissions;
}>();
const users = computed(() => {
return props.team.users as Array<UserWithMembership>;
});
const page = usePage();
const page = usePage<{
auth: {
user: User;
};
}>();
const addTeamMemberForm = useForm({
email: '',
role: null,
role: null as string | null,
});
const updateRoleForm = useForm({
role: null,
role: null as string | null,
});
const leaveTeamForm = useForm({});
const removeTeamMemberForm = useForm({});
const currentlyManagingRole = ref(false);
const managingRoleFor = ref(null);
const managingRoleFor = ref<User | null>(null);
const confirmingLeavingTeam = ref(false);
const teamMemberBeingRemoved = ref(null);
const teamMemberBeingRemoved = ref<User | null>(null);
const addTeamMember = () => {
addTeamMemberForm.post(route('team-members.store', props.team), {
addTeamMemberForm.post(route('team-members.store', props.team.id), {
errorBag: 'addTeamMember',
preserveScroll: true,
onSuccess: () => addTeamMemberForm.reset(),
});
};
const cancelTeamInvitation = (invitation) => {
router.delete(route('team-invitations.destroy', invitation), {
const cancelTeamInvitation = (invitation: OrganizationInvitation) => {
router.delete(route('team-invitations.destroy', invitation.id), {
preserveScroll: true,
});
};
const manageRole = (teamMember) => {
const manageRole = (teamMember: User & { membership: Membership }) => {
managingRoleFor.value = teamMember;
updateRoleForm.role = teamMember.membership.role;
currentlyManagingRole.value = true;
};
const updateRole = () => {
updateRoleForm.put(route('team-members.update', [props.team, managingRoleFor.value]), {
preserveScroll: true,
onSuccess: () => currentlyManagingRole.value = false,
});
updateRoleForm.put(
route('team-members.update', {
team: props.team.id,
user: managingRoleFor.value?.id,
}),
{
preserveScroll: true,
onSuccess: () => (currentlyManagingRole.value = false),
}
);
};
const confirmLeavingTeam = () => {
@@ -71,24 +93,32 @@ const confirmLeavingTeam = () => {
};
const leaveTeam = () => {
leaveTeamForm.delete(route('team-members.destroy', [props.team, page.props.auth.user]));
leaveTeamForm.delete(
route('team-members.destroy', [props.team.id, page.props.auth.user.id])
);
};
const confirmTeamMemberRemoval = (teamMember) => {
const confirmTeamMemberRemoval = (teamMember: User) => {
teamMemberBeingRemoved.value = teamMember;
};
const removeTeamMember = () => {
removeTeamMemberForm.delete(route('team-members.destroy', [props.team, teamMemberBeingRemoved.value]), {
errorBag: 'removeTeamMember',
preserveScroll: true,
preserveState: true,
onSuccess: () => teamMemberBeingRemoved.value = null,
});
removeTeamMemberForm.delete(
route('team-members.destroy', {
team: props.team.id,
user: teamMemberBeingRemoved.value?.id,
}),
{
errorBag: 'removeTeamMember',
preserveScroll: true,
preserveState: true,
onSuccess: () => (teamMemberBeingRemoved.value = null),
}
);
};
const displayableRole = (role) => {
return props.availableRoles.find(r => r.key === role).name;
const displayableRole = (role: string) => {
return props.availableRoles.find((r) => r.key === role)?.name;
};
</script>
@@ -99,18 +129,19 @@ const displayableRole = (role) => {
<!-- Add Organization Member -->
<FormSection @submitted="addTeamMember">
<template #title>
Add Team Member
</template>
<template #title> Add Team Member</template>
<template #description>
Add a new team member to your team, allowing them to collaborate with you.
Add a new team member to your team, allowing them to
collaborate with you.
</template>
<template #form>
<div class="col-span-6">
<div class="max-w-xl text-sm text-gray-600 dark:text-gray-400">
Please provide the email address of the person you would like to add to this team.
<div
class="max-w-xl text-sm text-gray-600 dark:text-gray-400">
Please provide the email address of the person you
would like to add to this team.
</div>
</div>
@@ -121,39 +152,75 @@ const displayableRole = (role) => {
id="email"
v-model="addTeamMemberForm.email"
type="email"
class="mt-1 block w-full"
/>
<InputError :message="addTeamMemberForm.errors.email" class="mt-2" />
class="mt-1 block w-full" />
<InputError
:message="addTeamMemberForm.errors.email"
class="mt-2" />
</div>
<!-- Role -->
<div v-if="availableRoles.length > 0" class="col-span-6 lg:col-span-4">
<div
v-if="availableRoles.length > 0"
class="col-span-6 lg:col-span-4">
<InputLabel for="roles" value="Role" />
<InputError :message="addTeamMemberForm.errors.role" class="mt-2" />
<InputError
:message="addTeamMemberForm.errors.role"
class="mt-2" />
<div class="relative z-0 mt-1 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer">
<div
class="relative z-0 mt-1 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer">
<button
v-for="(role, i) in availableRoles"
:key="role.key"
type="button"
class="relative px-4 py-3 inline-flex w-full rounded-lg focus:z-10 focus:outline-none focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600"
:class="{'border-t border-gray-200 dark:border-gray-700 focus:border-none rounded-t-none': i > 0, 'rounded-b-none': i != Object.keys(availableRoles).length - 1}"
@click="addTeamMemberForm.role = role.key"
>
<div :class="{'opacity-50': addTeamMemberForm.role && addTeamMemberForm.role != role.key}">
:class="{
'border-t border-gray-200 dark:border-gray-700 focus:border-none rounded-t-none':
i > 0,
'rounded-b-none':
i !=
Object.keys(availableRoles).length - 1,
}"
@click="addTeamMemberForm.role = role.key">
<div
:class="{
'opacity-50':
addTeamMemberForm.role &&
addTeamMemberForm.role != role.key,
}">
<!-- Role Name -->
<div class="flex items-center">
<div class="text-sm text-gray-600 dark:text-gray-400" :class="{'font-semibold': addTeamMemberForm.role == role.key}">
<div
class="text-sm text-gray-600 dark:text-gray-400"
:class="{
'font-semibold':
addTeamMemberForm.role ==
role.key,
}">
{{ role.name }}
</div>
<svg v-if="addTeamMemberForm.role == role.key" class="ms-2 h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
v-if="
addTeamMemberForm.role ==
role.key
"
class="ms-2 h-5 w-5 text-green-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<!-- Role Description -->
<div class="mt-2 text-xs text-gray-600 dark:text-gray-400 text-start">
<div
class="mt-2 text-xs text-gray-600 dark:text-gray-400 text-start">
{{ role.description }}
</div>
</div>
@@ -163,34 +230,45 @@ const displayableRole = (role) => {
</template>
<template #actions>
<ActionMessage :on="addTeamMemberForm.recentlySuccessful" class="me-3">
<ActionMessage
:on="addTeamMemberForm.recentlySuccessful"
class="me-3">
Added.
</ActionMessage>
<PrimaryButton :class="{ 'opacity-25': addTeamMemberForm.processing }" :disabled="addTeamMemberForm.processing">
<PrimaryButton
:class="{ 'opacity-25': addTeamMemberForm.processing }"
:disabled="addTeamMemberForm.processing">
Add
</PrimaryButton>
</template>
</FormSection>
</div>
<div v-if="team.team_invitations.length > 0 && userPermissions.canAddTeamMembers">
<div
v-if="
team.team_invitations.length > 0 &&
userPermissions.canAddTeamMembers
">
<SectionBorder />
<!-- Organization Member Invitations -->
<ActionSection class="mt-10 sm:mt-0">
<template #title>
Pending Team Invitations
</template>
<template #title> Pending Team Invitations</template>
<template #description>
These people have been invited to your team and have been sent an invitation email. They may join the team by accepting the email invitation.
These people have been invited to your team and have been
sent an invitation email. They may join the team by
accepting the email invitation.
</template>
<!-- Pending Organization Member Invitation List -->
<template #content>
<div class="space-y-6">
<div v-for="invitation in team.team_invitations" :key="invitation.id" class="flex items-center justify-between">
<div
v-for="invitation in team.team_invitations"
:key="invitation.id"
class="flex items-center justify-between">
<div class="text-gray-600 dark:text-gray-400">
{{ invitation.email }}
</div>
@@ -200,8 +278,7 @@ const displayableRole = (role) => {
<button
v-if="userPermissions.canRemoveTeamMembers"
class="cursor-pointer ms-6 text-sm text-red-500 focus:outline-none"
@click="cancelTeamInvitation(invitation)"
>
@click="cancelTeamInvitation(invitation)">
Cancel
</button>
</div>
@@ -211,14 +288,12 @@ const displayableRole = (role) => {
</ActionSection>
</div>
<div v-if="team.users.length > 0">
<div v-if="users.length > 0">
<SectionBorder />
<!-- Manage Organization Members -->
<ActionSection class="mt-10 sm:mt-0">
<template #title>
Team Members
</template>
<template #title> Team Members</template>
<template #description>
All of the people that are part of this team.
@@ -227,9 +302,15 @@ const displayableRole = (role) => {
<!-- Organization Member List -->
<template #content>
<div class="space-y-6">
<div v-for="user in team.users" :key="user.id" class="flex items-center justify-between">
<div
v-for="user in users"
:key="user.id"
class="flex items-center justify-between">
<div class="flex items-center">
<img class="w-8 h-8 rounded-full object-cover" :src="user.profile_photo_url" :alt="user.name">
<img
class="w-8 h-8 rounded-full object-cover"
:src="user.profile_photo_url"
:alt="user.name" />
<div class="ms-4 dark:text-white">
{{ user.name }}
</div>
@@ -238,32 +319,36 @@ const displayableRole = (role) => {
<div class="flex items-center">
<!-- Manage Organization Member Role -->
<button
v-if="userPermissions.canUpdateTeamMembers && availableRoles.length"
v-if="
userPermissions.canUpdateTeamMembers &&
availableRoles.length
"
class="ms-2 text-sm text-gray-400 underline"
@click="manageRole(user)"
>
@click="manageRole(user)">
{{ displayableRole(user.membership.role) }}
</button>
<div v-else-if="availableRoles.length" class="ms-2 text-sm text-gray-400">
<div
v-else-if="availableRoles.length"
class="ms-2 text-sm text-gray-400">
{{ displayableRole(user.membership.role) }}
</div>
<!-- Leave Organization -->
<button
v-if="$page.props.auth.user.id === user.id"
v-if="page.props.auth.user.id === user.id"
class="cursor-pointer ms-6 text-sm text-red-500"
@click="confirmLeavingTeam"
>
@click="confirmLeavingTeam">
Leave
</button>
<!-- Remove Organization Member -->
<button
v-else-if="userPermissions.canRemoveTeamMembers"
v-else-if="
userPermissions.canRemoveTeamMembers
"
class="cursor-pointer ms-6 text-sm text-red-500"
@click="confirmTeamMemberRemoval(user)"
>
@click="confirmTeamMemberRemoval(user)">
Remove
</button>
</div>
@@ -274,36 +359,64 @@ const displayableRole = (role) => {
</div>
<!-- Role Management Modal -->
<DialogModal :show="currentlyManagingRole" @close="currentlyManagingRole = false">
<template #title>
Manage Role
</template>
<DialogModal
:show="currentlyManagingRole"
@close="currentlyManagingRole = false">
<template #title> Manage Role</template>
<template #content>
<div v-if="managingRoleFor">
<div class="relative z-0 mt-1 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer">
<div
class="relative z-0 mt-1 border border-gray-200 dark:border-gray-700 rounded-lg cursor-pointer">
<button
v-for="(role, i) in availableRoles"
:key="role.key"
type="button"
class="relative px-4 py-3 inline-flex w-full rounded-lg focus:z-10 focus:outline-none focus:border-indigo-500 dark:focus:border-indigo-600 focus:ring-2 focus:ring-indigo-500 dark:focus:ring-indigo-600"
:class="{'border-t border-gray-200 dark:border-gray-700 focus:border-none rounded-t-none': i > 0, 'rounded-b-none': i !== Object.keys(availableRoles).length - 1}"
@click="updateRoleForm.role = role.key"
>
<div :class="{'opacity-50': updateRoleForm.role && updateRoleForm.role !== role.key}">
:class="{
'border-t border-gray-200 dark:border-gray-700 focus:border-none rounded-t-none':
i > 0,
'rounded-b-none':
i !==
Object.keys(availableRoles).length - 1,
}"
@click="updateRoleForm.role = role.key">
<div
:class="{
'opacity-50':
updateRoleForm.role &&
updateRoleForm.role !== role.key,
}">
<!-- Role Name -->
<div class="flex items-center">
<div class="text-sm text-gray-600 dark:text-gray-400" :class="{'font-semibold': updateRoleForm.role === role.key}">
<div
class="text-sm text-gray-600 dark:text-gray-400"
:class="{
'font-semibold':
updateRoleForm.role ===
role.key,
}">
{{ role.name }}
</div>
<svg v-if="updateRoleForm.role == role.key" class="ms-2 h-5 w-5 text-green-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
<svg
v-if="updateRoleForm.role == role.key"
class="ms-2 h-5 w-5 text-green-400"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<!-- Role Description -->
<div class="mt-2 text-xs text-gray-600 dark:text-gray-400">
<div
class="mt-2 text-xs text-gray-600 dark:text-gray-400">
{{ role.description }}
</div>
</div>
@@ -321,18 +434,17 @@ const displayableRole = (role) => {
class="ms-3"
:class="{ 'opacity-25': updateRoleForm.processing }"
:disabled="updateRoleForm.processing"
@click="updateRole"
>
@click="updateRole">
Save
</PrimaryButton>
</template>
</DialogModal>
<!-- Leave Organization Confirmation Modal -->
<ConfirmationModal :show="confirmingLeavingTeam" @close="confirmingLeavingTeam = false">
<template #title>
Leave Team
</template>
<ConfirmationModal
:show="confirmingLeavingTeam"
@close="confirmingLeavingTeam = false">
<template #title> Leave Team</template>
<template #content>
Are you sure you would like to leave this team?
@@ -347,18 +459,17 @@ const displayableRole = (role) => {
class="ms-3"
:class="{ 'opacity-25': leaveTeamForm.processing }"
:disabled="leaveTeamForm.processing"
@click="leaveTeam"
>
@click="leaveTeam">
Leave
</DangerButton>
</template>
</ConfirmationModal>
<!-- Remove Organization Member Confirmation Modal -->
<ConfirmationModal :show="teamMemberBeingRemoved" @close="teamMemberBeingRemoved = null">
<template #title>
Remove Team Member
</template>
<ConfirmationModal
:show="!!teamMemberBeingRemoved"
@close="teamMemberBeingRemoved = null">
<template #title> Remove Team Member</template>
<template #content>
Are you sure you would like to remove this person from the team?
@@ -373,8 +484,7 @@ const displayableRole = (role) => {
class="ms-3"
:class="{ 'opacity-25': removeTeamMemberForm.processing }"
:disabled="removeTeamMemberForm.processing"
@click="removeTeamMember"
>
@click="removeTeamMember">
Remove
</DangerButton>
</template>

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import FormSection from '@/Components/FormSection.vue';
@@ -6,18 +6,20 @@ import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
import type { Organization } from '@/types/models';
import type { Permissions } from '@/types/jetstream';
const props = defineProps({
team: Object,
permissions: Object,
});
const props = defineProps<{
team: Organization;
permissions: Permissions;
}>();
const form = useForm({
name: props.team.name,
});
const updateTeamName = () => {
form.put(route('teams.update', props.team), {
form.put(route('teams.update', props.team.id), {
errorBag: 'updateTeamName',
preserveScroll: true,
});
@@ -26,9 +28,7 @@ const updateTeamName = () => {
<template>
<FormSection @submitted="updateTeamName">
<template #title>
Team Name
</template>
<template #title> Team Name</template>
<template #description>
The team's name and owner information.
@@ -40,10 +40,15 @@ const updateTeamName = () => {
<InputLabel value="Team Owner" />
<div class="flex items-center mt-2">
<img class="w-12 h-12 rounded-full object-cover" :src="team.owner.profile_photo_url" :alt="team.owner.name">
<img
class="w-12 h-12 rounded-full object-cover"
:src="team.owner.profile_photo_url"
:alt="team.owner.name" />
<div class="ms-4 leading-tight">
<div class="text-gray-900 dark:text-white">{{ team.owner.name }}</div>
<div class="text-gray-900 dark:text-white">
{{ team.owner.name }}
</div>
<div class="text-gray-700 dark:text-gray-300 text-sm">
{{ team.owner.email }}
</div>
@@ -60,8 +65,7 @@ const updateTeamName = () => {
v-model="form.name"
type="text"
class="mt-1 block w-full"
:disabled="! permissions.canUpdateTeam"
/>
:disabled="!permissions.canUpdateTeam" />
<InputError :message="form.errors.name" class="mt-2" />
</div>
@@ -72,7 +76,9 @@ const updateTeamName = () => {
Saved.
</ActionMessage>
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
<PrimaryButton
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing">
Save
</PrimaryButton>
</template>

View File

@@ -1,21 +1,24 @@
<script setup>
<script setup lang="ts">
import AppLayout from '@/Layouts/AppLayout.vue';
import DeleteTeamForm from '@/Pages/Teams/Partials/DeleteTeamForm.vue';
import SectionBorder from '@/Components/SectionBorder.vue';
import TeamMemberManager from '@/Pages/Teams/Partials/TeamMemberManager.vue';
import UpdateTeamNameForm from '@/Pages/Teams/Partials/UpdateTeamNameForm.vue';
import type { Organization } from '@/types/models';
import type { Permissions, Role } from '@/types/jetstream';
defineProps({
team: Object,
availableRoles: Array,
permissions: Object,
});
defineProps<{
team: Organization;
availableRoles: Role[];
permissions: Permissions;
}>();
</script>
<template>
<AppLayout title="Team Settings">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
<h2
class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
Team Settings
</h2>
</template>
@@ -28,10 +31,10 @@ defineProps({
class="mt-10 sm:mt-0"
:team="team"
:available-roles="availableRoles"
:user-permissions="permissions"
/>
:user-permissions="permissions" />
<template v-if="permissions.canDeleteTeam && ! team.personal_team">
<template
v-if="permissions.canDeleteTeam && !team.personal_team">
<SectionBorder />
<DeleteTeamForm class="mt-10 sm:mt-0" :team="team" />

View File

@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
import { Head } from '@inertiajs/vue3';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
@@ -17,7 +17,9 @@ defineProps({
<AuthenticationCardLogo />
</div>
<div class="w-full sm:max-w-2xl mt-6 p-6 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg prose dark:prose-invert" v-html="terms" />
<div
class="w-full sm:max-w-2xl mt-6 p-6 bg-white dark:bg-gray-800 shadow-md overflow-hidden sm:rounded-lg prose dark:prose-invert"
v-html="terms" />
</div>
</div>
</div>

View File

@@ -1,5 +1,8 @@
<script setup>
import { Head, Link } from '@inertiajs/vue3';
<script setup lang="ts">
import { Head, Link, usePage } from '@inertiajs/vue3';
import route from 'ziggy-js';
import { computed } from 'vue';
import type { User } from '@/types/models';
defineProps({
canLogin: Boolean,
@@ -7,122 +10,312 @@ defineProps({
laravelVersion: String,
phpVersion: String,
});
const page = usePage<{
auth: {
user: User;
};
}>();
const user = computed(() => page?.props?.auth?.user);
</script>
<template>
<Head title="Welcome" />
<div class="relative sm:flex sm:justify-center sm:items-center min-h-screen bg-dots-darker bg-center bg-gray-100 dark:bg-dots-lighter dark:bg-gray-900 selection:bg-red-500 selection:text-white">
<div v-if="canLogin" class="sm:fixed sm:top-0 sm:end-0 p-6 text-end z-10">
<Link v-if="$page.props.auth.user" :href="route('dashboard')" class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Dashboard</Link>
<div
class="relative sm:flex sm:justify-center sm:items-center min-h-screen bg-dots-darker bg-center bg-gray-100 dark:bg-dots-lighter dark:bg-gray-900 selection:bg-red-500 selection:text-white">
<div
v-if="canLogin"
class="sm:fixed sm:top-0 sm:end-0 p-6 text-end z-10">
<Link
v-if="user"
:href="route('dashboard')"
class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
>Dashboard</Link
>
<template v-else>
<Link :href="route('login')" class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Log in</Link>
<Link
:href="route('login')"
class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
>Log in</Link
>
<Link v-if="canRegister" :href="route('register')" class="ms-4 font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Register</Link>
<Link
v-if="canRegister"
:href="route('register')"
class="ms-4 font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
>Register</Link
>
</template>
</div>
<div class="max-w-7xl mx-auto p-6 lg:p-8">
<div class="flex justify-center">
<svg viewBox="0 0 62 65" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-16 w-auto bg-gray-100 dark:bg-gray-900">
<path d="M61.8548 14.6253C61.8778 14.7102 61.8895 14.7978 61.8897 14.8858V28.5615C61.8898 28.737 61.8434 28.9095 61.7554 29.0614C61.6675 29.2132 61.5409 29.3392 61.3887 29.4265L49.9104 36.0351V49.1337C49.9104 49.4902 49.7209 49.8192 49.4118 49.9987L25.4519 63.7916C25.3971 63.8227 25.3372 63.8427 25.2774 63.8639C25.255 63.8714 25.2338 63.8851 25.2101 63.8913C25.0426 63.9354 24.8666 63.9354 24.6991 63.8913C24.6716 63.8838 24.6467 63.8689 24.6205 63.8589C24.5657 63.8389 24.5084 63.8215 24.456 63.7916L0.501061 49.9987C0.348882 49.9113 0.222437 49.7853 0.134469 49.6334C0.0465019 49.4816 0.000120578 49.3092 0 49.1337L0 8.10652C0 8.01678 0.0124642 7.92953 0.0348998 7.84477C0.0423783 7.8161 0.0598282 7.78993 0.0697995 7.76126C0.0884958 7.70891 0.105946 7.65531 0.133367 7.6067C0.152063 7.5743 0.179485 7.54812 0.20192 7.51821C0.230588 7.47832 0.256763 7.43719 0.290416 7.40229C0.319084 7.37362 0.356476 7.35243 0.388883 7.32751C0.425029 7.29759 0.457436 7.26518 0.498568 7.2415L12.4779 0.345059C12.6296 0.257786 12.8015 0.211853 12.9765 0.211853C13.1515 0.211853 13.3234 0.257786 13.475 0.345059L25.4531 7.2415H25.4556C25.4955 7.26643 25.5292 7.29759 25.5653 7.32626C25.5977 7.35119 25.6339 7.37362 25.6625 7.40104C25.6974 7.43719 25.7224 7.47832 25.7523 7.51821C25.7735 7.54812 25.8021 7.5743 25.8196 7.6067C25.8483 7.65656 25.8645 7.70891 25.8844 7.76126C25.8944 7.78993 25.9118 7.8161 25.9193 7.84602C25.9423 7.93096 25.954 8.01853 25.9542 8.10652V33.7317L35.9355 27.9844V14.8846C35.9355 14.7973 35.948 14.7088 35.9704 14.6253C35.9792 14.5954 35.9954 14.5692 36.0053 14.5405C36.0253 14.4882 36.0427 14.4346 36.0702 14.386C36.0888 14.3536 36.1163 14.3274 36.1375 14.2975C36.1674 14.2576 36.1923 14.2165 36.2272 14.1816C36.2559 14.1529 36.292 14.1317 36.3244 14.1068C36.3618 14.0769 36.3942 14.0445 36.4341 14.0208L48.4147 7.12434C48.5663 7.03694 48.7383 6.99094 48.9133 6.99094C49.0883 6.99094 49.2602 7.03694 49.4118 7.12434L61.3899 14.0208C61.4323 14.0457 61.4647 14.0769 61.5021 14.1055C61.5333 14.1305 61.5694 14.1529 61.5981 14.1803C61.633 14.2165 61.6579 14.2576 61.6878 14.2975C61.7103 14.3274 61.7377 14.3536 61.7551 14.386C61.7838 14.4346 61.8 14.4882 61.8199 14.5405C61.8312 14.5692 61.8474 14.5954 61.8548 14.6253ZM59.893 27.9844V16.6121L55.7013 19.0252L49.9104 22.3593V33.7317L59.8942 27.9844H59.893ZM47.9149 48.5566V37.1768L42.2187 40.4299L25.953 49.7133V61.2003L47.9149 48.5566ZM1.99677 9.83281V48.5566L23.9562 61.199V49.7145L12.4841 43.2219L12.4804 43.2194L12.4754 43.2169C12.4368 43.1945 12.4044 43.1621 12.3682 43.1347C12.3371 43.1097 12.3009 43.0898 12.2735 43.0624L12.271 43.0586C12.2386 43.0275 12.2162 42.9888 12.1887 42.9539C12.1638 42.9203 12.1339 42.8916 12.114 42.8567L12.1127 42.853C12.0903 42.8156 12.0766 42.7707 12.0604 42.7283C12.0442 42.6909 12.023 42.656 12.013 42.6161C12.0005 42.5688 11.998 42.5177 11.9931 42.4691C11.9881 42.4317 11.9781 42.3943 11.9781 42.3569V15.5801L6.18848 12.2446L1.99677 9.83281ZM12.9777 2.36177L2.99764 8.10652L12.9752 13.8513L22.9541 8.10527L12.9752 2.36177H12.9777ZM18.1678 38.2138L23.9574 34.8809V9.83281L19.7657 12.2459L13.9749 15.5801V40.6281L18.1678 38.2138ZM48.9133 9.14105L38.9344 14.8858L48.9133 20.6305L58.8909 14.8846L48.9133 9.14105ZM47.9149 22.3593L42.124 19.0252L37.9323 16.6121V27.9844L43.7219 31.3174L47.9149 33.7317V22.3593ZM24.9533 47.987L39.59 39.631L46.9065 35.4555L36.9352 29.7145L25.4544 36.3242L14.9907 42.3482L24.9533 47.987Z" fill="#FF2D20"/>
<svg
viewBox="0 0 62 65"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="h-16 w-auto bg-gray-100 dark:bg-gray-900">
<path
d="M61.8548 14.6253C61.8778 14.7102 61.8895 14.7978 61.8897 14.8858V28.5615C61.8898 28.737 61.8434 28.9095 61.7554 29.0614C61.6675 29.2132 61.5409 29.3392 61.3887 29.4265L49.9104 36.0351V49.1337C49.9104 49.4902 49.7209 49.8192 49.4118 49.9987L25.4519 63.7916C25.3971 63.8227 25.3372 63.8427 25.2774 63.8639C25.255 63.8714 25.2338 63.8851 25.2101 63.8913C25.0426 63.9354 24.8666 63.9354 24.6991 63.8913C24.6716 63.8838 24.6467 63.8689 24.6205 63.8589C24.5657 63.8389 24.5084 63.8215 24.456 63.7916L0.501061 49.9987C0.348882 49.9113 0.222437 49.7853 0.134469 49.6334C0.0465019 49.4816 0.000120578 49.3092 0 49.1337L0 8.10652C0 8.01678 0.0124642 7.92953 0.0348998 7.84477C0.0423783 7.8161 0.0598282 7.78993 0.0697995 7.76126C0.0884958 7.70891 0.105946 7.65531 0.133367 7.6067C0.152063 7.5743 0.179485 7.54812 0.20192 7.51821C0.230588 7.47832 0.256763 7.43719 0.290416 7.40229C0.319084 7.37362 0.356476 7.35243 0.388883 7.32751C0.425029 7.29759 0.457436 7.26518 0.498568 7.2415L12.4779 0.345059C12.6296 0.257786 12.8015 0.211853 12.9765 0.211853C13.1515 0.211853 13.3234 0.257786 13.475 0.345059L25.4531 7.2415H25.4556C25.4955 7.26643 25.5292 7.29759 25.5653 7.32626C25.5977 7.35119 25.6339 7.37362 25.6625 7.40104C25.6974 7.43719 25.7224 7.47832 25.7523 7.51821C25.7735 7.54812 25.8021 7.5743 25.8196 7.6067C25.8483 7.65656 25.8645 7.70891 25.8844 7.76126C25.8944 7.78993 25.9118 7.8161 25.9193 7.84602C25.9423 7.93096 25.954 8.01853 25.9542 8.10652V33.7317L35.9355 27.9844V14.8846C35.9355 14.7973 35.948 14.7088 35.9704 14.6253C35.9792 14.5954 35.9954 14.5692 36.0053 14.5405C36.0253 14.4882 36.0427 14.4346 36.0702 14.386C36.0888 14.3536 36.1163 14.3274 36.1375 14.2975C36.1674 14.2576 36.1923 14.2165 36.2272 14.1816C36.2559 14.1529 36.292 14.1317 36.3244 14.1068C36.3618 14.0769 36.3942 14.0445 36.4341 14.0208L48.4147 7.12434C48.5663 7.03694 48.7383 6.99094 48.9133 6.99094C49.0883 6.99094 49.2602 7.03694 49.4118 7.12434L61.3899 14.0208C61.4323 14.0457 61.4647 14.0769 61.5021 14.1055C61.5333 14.1305 61.5694 14.1529 61.5981 14.1803C61.633 14.2165 61.6579 14.2576 61.6878 14.2975C61.7103 14.3274 61.7377 14.3536 61.7551 14.386C61.7838 14.4346 61.8 14.4882 61.8199 14.5405C61.8312 14.5692 61.8474 14.5954 61.8548 14.6253ZM59.893 27.9844V16.6121L55.7013 19.0252L49.9104 22.3593V33.7317L59.8942 27.9844H59.893ZM47.9149 48.5566V37.1768L42.2187 40.4299L25.953 49.7133V61.2003L47.9149 48.5566ZM1.99677 9.83281V48.5566L23.9562 61.199V49.7145L12.4841 43.2219L12.4804 43.2194L12.4754 43.2169C12.4368 43.1945 12.4044 43.1621 12.3682 43.1347C12.3371 43.1097 12.3009 43.0898 12.2735 43.0624L12.271 43.0586C12.2386 43.0275 12.2162 42.9888 12.1887 42.9539C12.1638 42.9203 12.1339 42.8916 12.114 42.8567L12.1127 42.853C12.0903 42.8156 12.0766 42.7707 12.0604 42.7283C12.0442 42.6909 12.023 42.656 12.013 42.6161C12.0005 42.5688 11.998 42.5177 11.9931 42.4691C11.9881 42.4317 11.9781 42.3943 11.9781 42.3569V15.5801L6.18848 12.2446L1.99677 9.83281ZM12.9777 2.36177L2.99764 8.10652L12.9752 13.8513L22.9541 8.10527L12.9752 2.36177H12.9777ZM18.1678 38.2138L23.9574 34.8809V9.83281L19.7657 12.2459L13.9749 15.5801V40.6281L18.1678 38.2138ZM48.9133 9.14105L38.9344 14.8858L48.9133 20.6305L58.8909 14.8846L48.9133 9.14105ZM47.9149 22.3593L42.124 19.0252L37.9323 16.6121V27.9844L43.7219 31.3174L47.9149 33.7317V22.3593ZM24.9533 47.987L39.59 39.631L46.9065 35.4555L36.9352 29.7145L25.4544 36.3242L14.9907 42.3482L24.9533 47.987Z"
fill="#FF2D20" />
</svg>
</div>
<div class="mt-16">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6 lg:gap-8">
<a href="https://laravel.com/docs" class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex motion-safe:hover:scale-[1.01] transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
<a
href="https://laravel.com/docs"
class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex motion-safe:hover:scale-[1.01] transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
<div>
<div class="h-16 w-16 bg-red-50 dark:bg-red-800/20 flex items-center justify-center rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-7 h-7 stroke-red-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
<div
class="h-16 w-16 bg-red-50 dark:bg-red-800/20 flex items-center justify-center rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
class="w-7 h-7 stroke-red-500">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 6.042A8.967 8.967 0 006 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 016 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 016-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0018 18a8.967 8.967 0 00-6 2.292m0-14.25v14.25" />
</svg>
</div>
<h2 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">Documentation</h2>
<h2
class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
Documentation
</h2>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laravel has wonderful documentation covering every aspect of the framework. Whether you are a newcomer or have prior experience with Laravel, we recommend reading our documentation from beginning to end.
<p
class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laravel has wonderful documentation covering
every aspect of the framework. Whether you are a
newcomer or have prior experience with Laravel,
we recommend reading our documentation from
beginning to end.
</p>
</div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="self-center shrink-0 stroke-red-500 w-6 h-6 mx-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
class="self-center shrink-0 stroke-red-500 w-6 h-6 mx-6">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
</svg>
</a>
<a href="https://laracasts.com" class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex motion-safe:hover:scale-[1.01] transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
<a
href="https://laracasts.com"
class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex motion-safe:hover:scale-[1.01] transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
<div>
<div class="h-16 w-16 bg-red-50 dark:bg-red-800/20 flex items-center justify-center rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-7 h-7 stroke-red-500">
<path stroke-linecap="round" d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z" />
<div
class="h-16 w-16 bg-red-50 dark:bg-red-800/20 flex items-center justify-center rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
class="w-7 h-7 stroke-red-500">
<path
stroke-linecap="round"
d="M15.75 10.5l4.72-4.72a.75.75 0 011.28.53v11.38a.75.75 0 01-1.28.53l-4.72-4.72M4.5 18.75h9a2.25 2.25 0 002.25-2.25v-9a2.25 2.25 0 00-2.25-2.25h-9A2.25 2.25 0 002.25 7.5v9a2.25 2.25 0 002.25 2.25z" />
</svg>
</div>
<h2 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">Laracasts</h2>
<h2
class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
Laracasts
</h2>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laracasts offers thousands of video tutorials on Laravel, PHP, and JavaScript development. Check them out, see for yourself, and massively level up your development skills in the process.
<p
class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laracasts offers thousands of video tutorials on
Laravel, PHP, and JavaScript development. Check
them out, see for yourself, and massively level
up your development skills in the process.
</p>
</div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="self-center shrink-0 stroke-red-500 w-6 h-6 mx-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
class="self-center shrink-0 stroke-red-500 w-6 h-6 mx-6">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
</svg>
</a>
<a href="https://laravel-news.com" class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex motion-safe:hover:scale-[1.01] transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
<a
href="https://laravel-news.com"
class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex motion-safe:hover:scale-[1.01] transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
<div>
<div class="h-16 w-16 bg-red-50 dark:bg-red-800/20 flex items-center justify-center rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-7 h-7 stroke-red-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 01-2.25 2.25M16.5 7.5V18a2.25 2.25 0 002.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 002.25 2.25h13.5M6 7.5h3v3H6v-3z" />
<div
class="h-16 w-16 bg-red-50 dark:bg-red-800/20 flex items-center justify-center rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
class="w-7 h-7 stroke-red-500">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M12 7.5h1.5m-1.5 3h1.5m-7.5 3h7.5m-7.5 3h7.5m3-9h3.375c.621 0 1.125.504 1.125 1.125V18a2.25 2.25 0 01-2.25 2.25M16.5 7.5V18a2.25 2.25 0 002.25 2.25M16.5 7.5V4.875c0-.621-.504-1.125-1.125-1.125H4.125C3.504 3.75 3 4.254 3 4.875V18a2.25 2.25 0 002.25 2.25h13.5M6 7.5h3v3H6v-3z" />
</svg>
</div>
<h2 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">Laravel News</h2>
<h2
class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
Laravel News
</h2>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laravel News is a community driven portal and newsletter aggregating all of the latest and most important news in the Laravel ecosystem, including new package releases and tutorials.
<p
class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laravel News is a community driven portal and
newsletter aggregating all of the latest and
most important news in the Laravel ecosystem,
including new package releases and tutorials.
</p>
</div>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="self-center shrink-0 stroke-red-500 w-6 h-6 mx-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
class="self-center shrink-0 stroke-red-500 w-6 h-6 mx-6">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M4.5 12h15m0 0l-6.75-6.75M19.5 12l-6.75 6.75" />
</svg>
</a>
<div class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex motion-safe:hover:scale-[1.01] transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
<div
class="scale-100 p-6 bg-white dark:bg-gray-800/50 dark:bg-gradient-to-bl from-gray-700/50 via-transparent dark:ring-1 dark:ring-inset dark:ring-white/5 rounded-lg shadow-2xl shadow-gray-500/20 dark:shadow-none flex motion-safe:hover:scale-[1.01] transition-all duration-250 focus:outline focus:outline-2 focus:outline-red-500">
<div>
<div class="h-16 w-16 bg-red-50 dark:bg-red-800/20 flex items-center justify-center rounded-full">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="w-7 h-7 stroke-red-500">
<path stroke-linecap="round" stroke-linejoin="round" d="M6.115 5.19l.319 1.913A6 6 0 008.11 10.36L9.75 12l-.387.775c-.217.433-.132.956.21 1.298l1.348 1.348c.21.21.329.497.329.795v1.089c0 .426.24.815.622 1.006l.153.076c.433.217.956.132 1.298-.21l.723-.723a8.7 8.7 0 002.288-4.042 1.087 1.087 0 00-.358-1.099l-1.33-1.108c-.251-.21-.582-.299-.905-.245l-1.17.195a1.125 1.125 0 01-.98-.314l-.295-.295a1.125 1.125 0 010-1.591l.13-.132a1.125 1.125 0 011.3-.21l.603.302a.809.809 0 001.086-1.086L14.25 7.5l1.256-.837a4.5 4.5 0 001.528-1.732l.146-.292M6.115 5.19A9 9 0 1017.18 4.64M6.115 5.19A8.965 8.965 0 0112 3c1.929 0 3.716.607 5.18 1.64" />
<div
class="h-16 w-16 bg-red-50 dark:bg-red-800/20 flex items-center justify-center rounded-full">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
class="w-7 h-7 stroke-red-500">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M6.115 5.19l.319 1.913A6 6 0 008.11 10.36L9.75 12l-.387.775c-.217.433-.132.956.21 1.298l1.348 1.348c.21.21.329.497.329.795v1.089c0 .426.24.815.622 1.006l.153.076c.433.217.956.132 1.298-.21l.723-.723a8.7 8.7 0 002.288-4.042 1.087 1.087 0 00-.358-1.099l-1.33-1.108c-.251-.21-.582-.299-.905-.245l-1.17.195a1.125 1.125 0 01-.98-.314l-.295-.295a1.125 1.125 0 010-1.591l.13-.132a1.125 1.125 0 011.3-.21l.603.302a.809.809 0 001.086-1.086L14.25 7.5l1.256-.837a4.5 4.5 0 001.528-1.732l.146-.292M6.115 5.19A9 9 0 1017.18 4.64M6.115 5.19A8.965 8.965 0 0112 3c1.929 0 3.716.607 5.18 1.64" />
</svg>
</div>
<h2 class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">Vibrant Ecosystem</h2>
<h2
class="mt-6 text-xl font-semibold text-gray-900 dark:text-white">
Vibrant Ecosystem
</h2>
<p class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laravel's robust library of first-party tools and libraries, such as <a href="https://forge.laravel.com" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Forge</a>, <a href="https://vapor.laravel.com" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Vapor</a>, <a href="https://nova.laravel.com" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Nova</a>, and <a href="https://envoyer.io" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Envoyer</a> help you take your projects to the next level. Pair them with powerful open source libraries like <a href="https://laravel.com/docs/billing" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Cashier</a>, <a href="https://laravel.com/docs/dusk" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Dusk</a>, <a href="https://laravel.com/docs/broadcasting" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Echo</a>, <a href="https://laravel.com/docs/horizon" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Horizon</a>, <a href="https://laravel.com/docs/sanctum" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Sanctum</a>, <a href="https://laravel.com/docs/telescope" class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Telescope</a>, and more.
<p
class="mt-4 text-gray-500 dark:text-gray-400 text-sm leading-relaxed">
Laravel's robust library of first-party tools
and libraries, such as
<a
href="https://forge.laravel.com"
class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
>Forge</a
>,
<a
href="https://vapor.laravel.com"
class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
>Vapor</a
>,
<a
href="https://nova.laravel.com"
class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
>Nova</a
>, and
<a
href="https://envoyer.io"
class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
>Envoyer</a
>
help you take your projects to the next level.
Pair them with powerful open source libraries
like
<a
href="https://laravel.com/docs/billing"
class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
>Cashier</a
>,
<a
href="https://laravel.com/docs/dusk"
class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
>Dusk</a
>,
<a
href="https://laravel.com/docs/broadcasting"
class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
>Echo</a
>,
<a
href="https://laravel.com/docs/horizon"
class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
>Horizon</a
>,
<a
href="https://laravel.com/docs/sanctum"
class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
>Sanctum</a
>,
<a
href="https://laravel.com/docs/telescope"
class="underline hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500"
>Telescope</a
>, and more.
</p>
</div>
</div>
</div>
</div>
<div class="flex justify-center mt-16 px-6 sm:items-center sm:justify-between">
<div class="text-center text-sm text-gray-500 dark:text-gray-400 sm:text-start">
<div
class="flex justify-center mt-16 px-6 sm:items-center sm:justify-between">
<div
class="text-center text-sm text-gray-500 dark:text-gray-400 sm:text-start">
<div class="flex items-center gap-4">
<a href="https://github.com/sponsors/taylorotwell" class="group inline-flex items-center hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" class="-mt-px me-1 w-5 h-5 stroke-gray-400 dark:stroke-gray-600 group-hover:stroke-gray-600 dark:group-hover:stroke-gray-400">
<path stroke-linecap="round" stroke-linejoin="round" d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
<a
href="https://github.com/sponsors/taylorotwell"
class="group inline-flex items-center hover:text-gray-700 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
class="-mt-px me-1 w-5 h-5 stroke-gray-400 dark:stroke-gray-600 group-hover:stroke-gray-600 dark:group-hover:stroke-gray-400">
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21 8.25c0-2.485-2.099-4.5-4.688-4.5-1.935 0-3.597 1.126-4.312 2.733-.715-1.607-2.377-2.733-4.313-2.733C5.1 3.75 3 5.765 3 8.25c0 7.22 9 12 9 12s9-4.78 9-12z" />
</svg>
Sponsor
</a>
</div>
</div>
<div class="ms-4 text-center text-sm text-gray-500 dark:text-gray-400 sm:text-end sm:ms-0">
<div
class="ms-4 text-center text-sm text-gray-500 dark:text-gray-400 sm:text-end sm:ms-0">
Laravel v{{ laravelVersion }} (PHP v{{ phpVersion }})
</div>
</div>

View File

@@ -1,22 +1,28 @@
import './bootstrap';
import '../css/app.css';
import type { DefineComponent } from 'vue';
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/vue3';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
// @ts-expect-error - ziggy in composer is not typed
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';
const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
resolve: (name) =>
resolvePageComponent(
`./Pages/${name}.vue`,
import.meta.glob<DefineComponent>('./Pages/**/*.vue')
),
setup({ el, App, props, plugin }) {
return createApp({ render: () => h(App, props) })
createApp({ render: () => h(App, props) })
.use(plugin)
.use(ZiggyVue)
.mount(el);
},
progress: {
color: '#4B5563',
},

View File

@@ -0,0 +1 @@
export type HtmlButtonType = 'button' | 'submit' | 'reset';

22
resources/js/types/global.d.ts vendored Normal file
View File

@@ -0,0 +1,22 @@
import { PageProps as InertiaPageProps } from '@inertiajs/core';
import { AxiosInstance } from 'axios';
import ziggyRoute from 'ziggy-js';
import { PageProps as AppPageProps } from './';
declare global {
interface Window {
axios: AxiosInstance;
}
let route: typeof ziggyRoute;
}
declare module 'vue' {
interface ComponentCustomProperties {
route: typeof ziggyRoute;
}
}
declare module '@inertiajs/core' {
interface PageProps extends InertiaPageProps, AppPageProps {}
}

19
resources/js/types/inertia.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
export {};
declare global {
export namespace inertia {
export interface Props {
user: {
id: number;
name: string;
email: string;
created_at: Date;
updated_at: Date;
};
jetstream: {
[key: string]: boolean;
};
errorBags: unknown;
errors: unknown;
}
}
}

View File

@@ -0,0 +1,42 @@
import type { User } from '@/types/models';
export interface Permissions {
canAddTeamMembers: boolean;
canDeleteTeam: boolean;
canRemoveTeamMembers: boolean;
canUpdateTeam: boolean;
canUpdateTeamMembers: boolean;
}
export interface Session {
agent: {
platform: string;
browser: string;
is_desktop: boolean;
};
ip_address: string;
is_current_device: string;
last_active: boolean;
}
export interface Membership {
role: string;
}
export interface Role {
key: string;
name: string;
description: string;
}
export type JetstreamUser = User & {
two_factor_enabled: boolean;
};
export interface Token {
name: string;
token: string;
abilities: string[];
id: string;
last_used_ago: string;
}

View File

@@ -0,0 +1,132 @@
export interface Client {
// columns
id: string;
name: string;
organization_id: string;
created_at: string | null;
updated_at: string | null;
// relations
organization: Organization;
}
export interface Membership {
// columns
id: string;
organization_id: string;
user_id: string;
role: string | null;
created_at: string | null;
updated_at: string | null;
}
export interface Organization {
// columns
id: string;
user_id: string;
name: string;
personal_team: boolean;
created_at: string | null;
updated_at: string | null;
// relations
owner: User;
users: User[];
team_invitations: OrganizationInvitation[];
}
export interface OrganizationInvitation {
// columns
id: string;
organization_id: string;
email: string;
role: string | null;
created_at: string | null;
updated_at: string | null;
// relations
organization: Organization;
team: Organization;
}
export interface Project {
// columns
id: string;
name: string;
color: string;
client_id: string | null;
organization_id: string;
created_at: string | null;
updated_at: string | null;
// relations
organization: Organization;
client: Client;
tasks: Task[];
}
export interface Tag {
// columns
id: string;
name: string;
organization_id: string;
created_at: string | null;
updated_at: string | null;
// relations
organization: Organization;
}
export interface Task {
// columns
id: string;
name: string;
project_id: string;
organization_id: string;
created_at: string | null;
updated_at: string | null;
// relations
project: Project;
organization: Organization;
}
export interface TimeEntry {
// columns
id: string;
description: string;
start: string;
end: string | null;
billable: boolean;
user_id: string;
organization_id: string;
project_id: string | null;
task_id: string | null;
tags: string[] | null;
created_at: string | null;
updated_at: string | null;
// relations
user: User;
organization: Organization;
project: Project;
task: Task;
}
export interface User {
// columns
id: string;
name: string;
email: string;
email_verified_at: string | null;
password?: string;
remember_token?: string | null;
current_team_id: string | null;
profile_photo_path: string | null;
created_at: string | null;
updated_at: string | null;
two_factor_secret?: string | null;
two_factor_recovery_codes?: string | null;
two_factor_confirmed_at: string | null;
// mutators
profile_photo_url: string;
// relations
organizations: Organization[];
clients: Client[];
current_team: Organization;
owned_teams: Organization[];
teams: Organization[];
}

1
resources/js/types/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

5
resources/js/types/vue-shim.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
declare module '*.vue' {
import { defineComponent } from 'vue';
const component: ReturnType<typeof defineComponent>;
export default component;
}

5
resources/js/types/ziggy.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
import routeFn from 'ziggy-js';
declare global {
let route: typeof routeFn;
}

147
resources/js/ziggy.d.ts vendored Normal file
View File

@@ -0,0 +1,147 @@
/* This file is generated by Ziggy. */
declare module 'ziggy-js' {
interface RouteList {
'filament.exports.download': [
{
'name': 'export';
'binding': 'id';
},
];
'filament.imports.failed-rows.download': [
{
'name': 'import';
'binding': 'id';
},
];
'filament.admin.auth.logout': [];
'filament.admin.pages.dashboard': [];
'login': [];
'logout': [];
'password.request': [];
'password.reset': [
{
'name': 'token';
},
];
'password.email': [];
'password.update': [];
'register': [];
'verification.notice': [];
'verification.verify': [
{
'name': 'id';
},
{
'name': 'hash';
},
];
'verification.send': [];
'user-profile-information.update': [];
'user-password.update': [];
'password.confirmation': [];
'password.confirm': [];
'two-factor.login': [];
'two-factor.enable': [];
'two-factor.confirm': [];
'two-factor.disable': [];
'two-factor.qr-code': [];
'two-factor.secret-key': [];
'two-factor.recovery-codes': [];
'profile.show': [];
'other-browser-sessions.destroy': [];
'current-user-photo.destroy': [];
'current-user.destroy': [];
'teams.create': [];
'teams.store': [];
'teams.show': [
{
'name': 'team';
},
];
'teams.update': [
{
'name': 'team';
},
];
'teams.destroy': [
{
'name': 'team';
},
];
'current-team.update': [];
'team-members.store': [
{
'name': 'team';
},
];
'team-members.update': [
{
'name': 'team';
},
{
'name': 'user';
},
];
'team-members.destroy': [
{
'name': 'team';
},
{
'name': 'user';
},
];
'team-invitations.accept': [
{
'name': 'invitation';
},
];
'team-invitations.destroy': [
{
'name': 'invitation';
},
];
'passport.token': [];
'passport.authorizations.authorize': [];
'passport.token.refresh': [];
'passport.authorizations.approve': [];
'passport.authorizations.deny': [];
'passport.tokens.index': [];
'passport.tokens.destroy': [
{
'name': 'token_id';
},
];
'passport.clients.index': [];
'passport.clients.store': [];
'passport.clients.update': [
{
'name': 'client_id';
},
];
'passport.clients.destroy': [
{
'name': 'client_id';
},
];
'passport.scopes.index': [];
'passport.personal.tokens.index': [];
'passport.personal.tokens.store': [];
'passport.personal.tokens.destroy': [
{
'name': 'token_id';
},
];
'livewire.update': [];
'livewire.upload-file': [];
'livewire.preview-file': [
{
'name': 'filename';
},
];
'ignition.healthCheck': [];
'ignition.executeSolution': [];
'ignition.updateConfig': [];
'dashboard': [];
}
}
export {};

238
resources/js/ziggy.js Normal file
View File

@@ -0,0 +1,238 @@
const Ziggy = {
'url': 'http://localhost',
'port': null,
'defaults': {},
'routes': {
'filament.exports.download': {
'uri': 'filament/exports/{export}/download',
'methods': ['GET', 'HEAD'],
'parameters': ['export'],
'bindings': { 'export': 'id' },
},
'filament.imports.failed-rows.download': {
'uri': 'filament/imports/{import}/failed-rows/download',
'methods': ['GET', 'HEAD'],
'parameters': ['import'],
'bindings': { 'import': 'id' },
},
'filament.admin.auth.logout': {
'uri': 'admin/logout',
'methods': ['POST'],
},
'filament.admin.pages.dashboard': {
'uri': 'admin',
'methods': ['GET', 'HEAD'],
},
'login': { 'uri': 'login', 'methods': ['GET', 'HEAD'] },
'logout': { 'uri': 'logout', 'methods': ['POST'] },
'password.request': {
'uri': 'forgot-password',
'methods': ['GET', 'HEAD'],
},
'password.reset': {
'uri': 'reset-password/{token}',
'methods': ['GET', 'HEAD'],
'parameters': ['token'],
},
'password.email': { 'uri': 'forgot-password', 'methods': ['POST'] },
'password.update': { 'uri': 'reset-password', 'methods': ['POST'] },
'register': { 'uri': 'register', 'methods': ['GET', 'HEAD'] },
'verification.notice': {
'uri': 'email/verify',
'methods': ['GET', 'HEAD'],
},
'verification.verify': {
'uri': 'email/verify/{id}/{hash}',
'methods': ['GET', 'HEAD'],
'parameters': ['id', 'hash'],
},
'verification.send': {
'uri': 'email/verification-notification',
'methods': ['POST'],
},
'user-profile-information.update': {
'uri': 'user/profile-information',
'methods': ['PUT'],
},
'user-password.update': { 'uri': 'user/password', 'methods': ['PUT'] },
'password.confirmation': {
'uri': 'user/confirmed-password-status',
'methods': ['GET', 'HEAD'],
},
'password.confirm': {
'uri': 'user/confirm-password',
'methods': ['POST'],
},
'two-factor.login': {
'uri': 'two-factor-challenge',
'methods': ['GET', 'HEAD'],
},
'two-factor.enable': {
'uri': 'user/two-factor-authentication',
'methods': ['POST'],
},
'two-factor.confirm': {
'uri': 'user/confirmed-two-factor-authentication',
'methods': ['POST'],
},
'two-factor.disable': {
'uri': 'user/two-factor-authentication',
'methods': ['DELETE'],
},
'two-factor.qr-code': {
'uri': 'user/two-factor-qr-code',
'methods': ['GET', 'HEAD'],
},
'two-factor.secret-key': {
'uri': 'user/two-factor-secret-key',
'methods': ['GET', 'HEAD'],
},
'two-factor.recovery-codes': {
'uri': 'user/two-factor-recovery-codes',
'methods': ['GET', 'HEAD'],
},
'profile.show': { 'uri': 'user/profile', 'methods': ['GET', 'HEAD'] },
'other-browser-sessions.destroy': {
'uri': 'user/other-browser-sessions',
'methods': ['DELETE'],
},
'current-user-photo.destroy': {
'uri': 'user/profile-photo',
'methods': ['DELETE'],
},
'current-user.destroy': { 'uri': 'user', 'methods': ['DELETE'] },
'teams.create': { 'uri': 'teams/create', 'methods': ['GET', 'HEAD'] },
'teams.store': { 'uri': 'teams', 'methods': ['POST'] },
'teams.show': {
'uri': 'teams/{team}',
'methods': ['GET', 'HEAD'],
'parameters': ['team'],
},
'teams.update': {
'uri': 'teams/{team}',
'methods': ['PUT'],
'parameters': ['team'],
},
'teams.destroy': {
'uri': 'teams/{team}',
'methods': ['DELETE'],
'parameters': ['team'],
},
'current-team.update': { 'uri': 'current-team', 'methods': ['PUT'] },
'team-members.store': {
'uri': 'teams/{team}/members',
'methods': ['POST'],
'parameters': ['team'],
},
'team-members.update': {
'uri': 'teams/{team}/members/{user}',
'methods': ['PUT'],
'parameters': ['team', 'user'],
},
'team-members.destroy': {
'uri': 'teams/{team}/members/{user}',
'methods': ['DELETE'],
'parameters': ['team', 'user'],
},
'team-invitations.accept': {
'uri': 'team-invitations/{invitation}',
'methods': ['GET', 'HEAD'],
'parameters': ['invitation'],
},
'team-invitations.destroy': {
'uri': 'team-invitations/{invitation}',
'methods': ['DELETE'],
'parameters': ['invitation'],
},
'passport.token': { 'uri': 'oauth/token', 'methods': ['POST'] },
'passport.authorizations.authorize': {
'uri': 'oauth/authorize',
'methods': ['GET', 'HEAD'],
},
'passport.token.refresh': {
'uri': 'oauth/token/refresh',
'methods': ['POST'],
},
'passport.authorizations.approve': {
'uri': 'oauth/authorize',
'methods': ['POST'],
},
'passport.authorizations.deny': {
'uri': 'oauth/authorize',
'methods': ['DELETE'],
},
'passport.tokens.index': {
'uri': 'oauth/tokens',
'methods': ['GET', 'HEAD'],
},
'passport.tokens.destroy': {
'uri': 'oauth/tokens/{token_id}',
'methods': ['DELETE'],
'parameters': ['token_id'],
},
'passport.clients.index': {
'uri': 'oauth/clients',
'methods': ['GET', 'HEAD'],
},
'passport.clients.store': {
'uri': 'oauth/clients',
'methods': ['POST'],
},
'passport.clients.update': {
'uri': 'oauth/clients/{client_id}',
'methods': ['PUT'],
'parameters': ['client_id'],
},
'passport.clients.destroy': {
'uri': 'oauth/clients/{client_id}',
'methods': ['DELETE'],
'parameters': ['client_id'],
},
'passport.scopes.index': {
'uri': 'oauth/scopes',
'methods': ['GET', 'HEAD'],
},
'passport.personal.tokens.index': {
'uri': 'oauth/personal-access-tokens',
'methods': ['GET', 'HEAD'],
},
'passport.personal.tokens.store': {
'uri': 'oauth/personal-access-tokens',
'methods': ['POST'],
},
'passport.personal.tokens.destroy': {
'uri': 'oauth/personal-access-tokens/{token_id}',
'methods': ['DELETE'],
'parameters': ['token_id'],
},
'livewire.update': { 'uri': 'livewire/update', 'methods': ['POST'] },
'livewire.upload-file': {
'uri': 'livewire/upload-file',
'methods': ['POST'],
},
'livewire.preview-file': {
'uri': 'livewire/preview-file/{filename}',
'methods': ['GET', 'HEAD'],
'parameters': ['filename'],
},
'ignition.healthCheck': {
'uri': '_ignition/health-check',
'methods': ['GET', 'HEAD'],
},
'ignition.executeSolution': {
'uri': '_ignition/execute-solution',
'methods': ['POST'],
},
'ignition.updateConfig': {
'uri': '_ignition/update-config',
'methods': ['POST'],
},
'dashboard': { 'uri': 'dashboard', 'methods': ['GET', 'HEAD'] },
},
};
if (typeof window !== 'undefined' && typeof window.Ziggy !== 'undefined') {
Object.assign(Ziggy.routes, window.Ziggy.routes);
}
export { Ziggy };

View File

@@ -12,7 +12,7 @@
<!-- Scripts -->
@routes
@vite(['resources/js/app.js', "resources/js/Pages/{$page['component']}.vue"])
@vite(['resources/js/app.ts', "resources/js/Pages/{$page['component']}.vue"])
@inertiaHead
</head>
<body class="font-sans antialiased">

23
tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"extends": "@vue/tsconfig/tsconfig.json",
"exclude": [
"vendor/**/*"
],
"compilerOptions": {
"paths": {
"@/*": ["./resources/js/*"],
"ziggy-js": ["./vendor/tightenco/ziggy"]
}
},
"skipLibCheck": true,
"typeRoots": [
"./node_modules/@types",
"resources/js/types"
],
"include": [
"resources/js/**/*.ts",
"resources/js/**/*.d.ts",
"resources/js/**/*.vue",
"resources/js/ziggy.d.ts",
]
}

View File

@@ -1,11 +1,12 @@
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import vue from '@vitejs/plugin-vue';
import checker from 'vite-plugin-checker';
export default defineConfig({
plugins: [
laravel({
input: 'resources/js/app.js',
input: 'resources/js/app.ts',
refresh: true,
}),
vue({
@@ -16,5 +17,11 @@ export default defineConfig({
},
},
}),
checker({
// e.g. use TypeScript check
typescript: true,
vueTsc: true,
lintCommand: 'eslint "./**/*.{ts,vue}"',
}),
],
});