initial commit

This commit is contained in:
2022-09-23 08:46:47 -06:00
commit 1921efce37
191 changed files with 22614 additions and 0 deletions

View File

@ -0,0 +1,15 @@
<script setup>
defineProps({
on: Boolean,
});
</script>
<template>
<div>
<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">
<slot />
</div>
</transition>
</div>
</template>

View File

@ -0,0 +1,22 @@
<script setup>
import SectionTitle from './SectionTitle.vue';
</script>
<template>
<div class="md:grid md:grid-cols-3 md:gap-6">
<SectionTitle>
<template #title>
<slot name="title" />
</template>
<template #description>
<slot name="description" />
</template>
</SectionTitle>
<div class="mt-5 md:mt-0 md:col-span-2">
<div class="px-4 py-5 sm:p-6 bg-white shadow sm:rounded-lg">
<slot name="content" />
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,7 @@
<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" fill="#000" />
<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

@ -0,0 +1,6 @@
<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" />
</svg>
</template>

View File

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

View File

@ -0,0 +1,17 @@
<script setup>
import { Link } from '@inertiajs/inertia-vue3';
</script>
<template>
<Link :href="'/'">
<svg
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" />
</svg>
</Link>
</template>

View File

@ -0,0 +1,87 @@
<script setup>
import { computed, ref, watch } from 'vue';
import { usePage } from '@inertiajs/inertia-vue3';
const show = ref(true);
const style = computed(() => usePage().props.value.jetstream.flash?.bannerStyle || 'success');
const message = computed(() => usePage().props.value.jetstream.flash?.banner || '');
watch(message, async () => {
show.value = true;
});
</script>
<template>
<div>
<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="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12l2 2 4-4m6 2a9 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="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</span>
<p class="ml-3 font-medium text-sm text-white truncate">
{{ message }}
</p>
</div>
<div class="shrink-0 sm:ml-3">
<button
type="button"
class="-mr-1 flex p-2 rounded-md focus:outline-none sm:-mr-2 transition"
: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="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,36 @@
<script setup>
import { computed } from 'vue';
const emit = defineEmits(['update:checked']);
const props = defineProps({
checked: {
type: [Array, Boolean],
default: false,
},
value: {
type: String,
default: null,
},
});
const proxyChecked = computed({
get() {
return props.checked;
},
set(val) {
emit('update:checked', val);
},
});
</script>
<template>
<input
v-model="proxyChecked"
type="checkbox"
:value="value"
class="rounded border-gray-300 text-indigo-600 shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
>
</template>

View File

@ -0,0 +1,67 @@
<script setup>
import Modal from './Modal.vue';
const emit = defineEmits(['close']);
defineProps({
show: {
type: Boolean,
default: false,
},
maxWidth: {
type: String,
default: '2xl',
},
closeable: {
type: Boolean,
default: true,
},
});
const close = () => {
emit('close');
};
</script>
<template>
<Modal
:show="show"
:max-width="maxWidth"
:closeable="closeable"
@close="close"
>
<div class="bg-white 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"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
/>
</svg>
</div>
<div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 class="text-lg">
<slot name="title" />
</h3>
<div class="mt-2">
<slot name="content" />
</div>
</div>
</div>
</div>
<div class="flex flex-row justify-end px-6 py-4 bg-gray-100 text-right">
<slot name="footer" />
</div>
</Modal>
</template>

View File

@ -0,0 +1,117 @@
<script setup>
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';
const emit = defineEmits(['confirmed']);
defineProps({
title: {
type: String,
default: 'Confirm Password',
},
content: {
type: String,
default: 'For your security, please confirm your password to continue.',
},
button: {
type: String,
default: 'Confirm',
},
});
const confirmingPassword = ref(false);
const form = reactive({
password: '',
error: '',
processing: false,
});
const passwordInput = ref(null);
const startConfirmingPassword = () => {
axios.get(route('password.confirmation')).then(response => {
if (response.data.confirmed) {
emit('confirmed');
} else {
confirmingPassword.value = true;
setTimeout(() => passwordInput.value.focus(), 250);
}
});
};
const confirmPassword = () => {
form.processing = true;
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();
});
};
const closeModal = () => {
confirmingPassword.value = false;
form.password = '';
form.error = '';
};
</script>
<template>
<span>
<span @click="startConfirmingPassword">
<slot />
</span>
<DialogModal :show="confirmingPassword" @close="closeModal">
<template #title>
{{ title }}
</template>
<template #content>
{{ content }}
<div class="mt-4">
<TextInput
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-3/4"
placeholder="Password"
@keyup.enter="confirmPassword"
/>
<InputError :message="form.error" class="mt-2" />
</div>
</template>
<template #footer>
<SecondaryButton @click="closeModal">
Cancel
</SecondaryButton>
<PrimaryButton
class="ml-3"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="confirmPassword"
>
{{ button }}
</PrimaryButton>
</template>
</DialogModal>
</span>
</template>

View File

@ -0,0 +1,14 @@
<script setup>
defineProps({
type: {
type: String,
default: 'button',
},
});
</script>
<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 focus:outline-none focus:border-red-700 focus:ring focus:ring-red-200 active:bg-red-600 disabled:opacity-25 transition">
<slot />
</button>
</template>

View File

@ -0,0 +1,47 @@
<script setup>
import Modal from './Modal.vue';
const emit = defineEmits(['close']);
defineProps({
show: {
type: Boolean,
default: false,
},
maxWidth: {
type: String,
default: '2xl',
},
closeable: {
type: Boolean,
default: true,
},
});
const close = () => {
emit('close');
};
</script>
<template>
<Modal
:show="show"
:max-width="maxWidth"
:closeable="closeable"
@close="close"
>
<div class="px-6 py-4">
<div class="text-lg">
<slot name="title" />
</div>
<div class="mt-4">
<slot name="content" />
</div>
</div>
<div class="flex flex-row justify-end px-6 py-4 bg-gray-100 text-right">
<slot name="footer" />
</div>
</Modal>
</template>

View File

@ -0,0 +1,79 @@
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
const props = defineProps({
align: {
type: String,
default: 'right',
},
width: {
type: String,
default: '48',
},
contentClasses: {
type: Array,
default: () => ['py-1', 'bg-white'],
},
});
let open = ref(false);
const closeOnEscape = (e) => {
if (open.value && e.key === 'Escape') {
open.value = false;
}
};
onMounted(() => document.addEventListener('keydown', closeOnEscape));
onUnmounted(() => document.removeEventListener('keydown', closeOnEscape));
const widthClass = computed(() => {
return {
'48': 'w-48',
}[props.width.toString()];
});
const alignmentClasses = computed(() => {
if (props.align === 'left') {
return 'origin-top-left left-0';
}
if (props.align === 'right') {
return 'origin-top-right right-0';
}
return 'origin-top';
});
</script>
<template>
<div class="relative">
<div @click="open = ! open">
<slot name="trigger" />
</div>
<!-- Full Screen Dropdown Overlay -->
<div v-show="open" class="fixed inset-0 z-40" @click="open = false" />
<transition
enter-active-class="transition ease-out duration-200"
enter-from-class="transform opacity-0 scale-95"
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"
>
<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">
<slot name="content" />
</div>
</div>
</transition>
</div>
</template>

View File

@ -0,0 +1,24 @@
<script setup>
import { Link } from '@inertiajs/inertia-vue3';
defineProps({
href: String,
as: String,
});
</script>
<template>
<div>
<button v-if="as == 'button'" type="submit" class="block w-full px-4 py-2 text-sm leading-5 text-gray-700 text-left hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition">
<slot />
</button>
<a v-else-if="as =='a'" :href="href" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition">
<slot />
</a>
<Link v-else :href="href" class="block px-4 py-2 text-sm leading-5 text-gray-700 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 transition">
<slot />
</Link>
</div>
</template>

View File

@ -0,0 +1,38 @@
<script setup>
import { computed, useSlots } from 'vue';
import SectionTitle from './SectionTitle.vue';
defineEmits(['submitted']);
const hasActions = computed(() => !! useSlots().actions);
</script>
<template>
<div class="md:grid md:grid-cols-3 md:gap-6">
<SectionTitle>
<template #title>
<slot name="title" />
</template>
<template #description>
<slot name="description" />
</template>
</SectionTitle>
<div class="mt-5 md:mt-0 md:col-span-2">
<form @submit.prevent="$emit('submitted')">
<div
class="px-4 py-5 bg-white sm:p-6 shadow"
: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 text-right sm:px-6 shadow sm:rounded-bl-md sm:rounded-br-md">
<slot name="actions" />
</div>
</form>
</div>
</div>
</template>

View File

@ -0,0 +1,13 @@
<script setup>
defineProps({
message: String,
});
</script>
<template>
<div v-show="message">
<p class="text-sm text-red-600">
{{ message }}
</p>
</div>
</template>

View File

@ -0,0 +1,12 @@
<script setup>
defineProps({
value: String,
});
</script>
<template>
<label class="block font-medium text-sm text-gray-700">
<span v-if="value">{{ value }}</span>
<span v-else><slot /></span>
</label>
</template>

View File

@ -0,0 +1,91 @@
<script setup>
import { computed, onMounted, onUnmounted, watch } from 'vue';
const props = defineProps({
show: {
type: Boolean,
default: false,
},
maxWidth: {
type: String,
default: '2xl',
},
closeable: {
type: Boolean,
default: true,
},
});
const emit = defineEmits(['close']);
watch(() => props.show, () => {
if (props.show) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = null;
}
});
const close = () => {
if (props.closeable) {
emit('close');
}
};
const closeOnEscape = (e) => {
if (e.key === 'Escape' && props.show) {
close();
}
};
onMounted(() => document.addEventListener('keydown', closeOnEscape));
onUnmounted(() => {
document.removeEventListener('keydown', closeOnEscape);
document.body.style.overflow = null;
});
const maxWidthClass = computed(() => {
return {
'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];
});
</script>
<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>
<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 opacity-75" />
</div>
</transition>
<transition
enter-active-class="ease-out duration-300"
enter-from-class="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
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 rounded-lg overflow-hidden shadow-xl transform transition-all sm:w-full sm:mx-auto" :class="maxWidthClass">
<slot v-if="show" />
</div>
</transition>
</div>
</transition>
</teleport>
</template>

View File

@ -0,0 +1,21 @@
<script setup>
import { computed } from 'vue';
import { Link } from '@inertiajs/inertia-vue3';
const props = defineProps({
href: String,
active: Boolean,
});
const classes = computed(() => {
return props.active
? 'inline-flex items-center px-1 pt-1 border-b-2 border-indigo-400 text-sm font-medium leading-5 text-gray-900 focus:outline-none focus:border-indigo-700 transition'
: 'inline-flex items-center px-1 pt-1 border-b-2 border-transparent text-sm font-medium leading-5 text-gray-500 hover:text-gray-700 hover:border-gray-300 focus:outline-none focus:text-gray-700 focus:border-gray-300 transition';
});
</script>
<template>
<Link :href="href" :class="classes">
<slot />
</Link>
</template>

View File

@ -0,0 +1,14 @@
<script setup>
defineProps({
type: {
type: String,
default: 'submit',
},
});
</script>
<template>
<button :type="type" class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700 active:bg-gray-900 focus:outline-none focus:border-gray-900 focus:ring focus:ring-gray-300 disabled:opacity-25 transition">
<slot />
</button>
</template>

View File

@ -0,0 +1,28 @@
<script setup>
import { computed } from 'vue';
import { Link } from '@inertiajs/inertia-vue3';
const props = defineProps({
active: Boolean,
href: String,
as: String,
});
const classes = computed(() => {
return props.active
? 'block pl-3 pr-4 py-2 border-l-4 border-indigo-400 text-base font-medium text-indigo-700 bg-indigo-50 focus:outline-none focus:text-indigo-800 focus:bg-indigo-100 focus:border-indigo-700 transition'
: 'block pl-3 pr-4 py-2 border-l-4 border-transparent text-base font-medium text-gray-600 hover:text-gray-800 hover:bg-gray-50 hover:border-gray-300 focus:outline-none focus:text-gray-800 focus:bg-gray-50 focus:border-gray-300 transition';
});
</script>
<template>
<div>
<button v-if="as == 'button'" :class="classes" class="w-full text-left">
<slot />
</button>
<Link v-else :href="href" :class="classes">
<slot />
</Link>
</div>
</template>

View File

@ -0,0 +1,14 @@
<script setup>
defineProps({
type: {
type: String,
default: 'button',
},
});
</script>
<template>
<button :type="type" class="inline-flex items-center px-4 py-2 bg-white border border-gray-300 rounded-md font-semibold text-xs text-gray-700 uppercase tracking-widest shadow-sm hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:ring focus:ring-blue-200 active:text-gray-800 active:bg-gray-50 disabled:opacity-25 transition">
<slot />
</button>
</template>

View File

@ -0,0 +1,7 @@
<template>
<div class="hidden sm:block">
<div class="py-8">
<div class="border-t border-gray-200" />
</div>
</div>
</template>

View File

@ -0,0 +1,17 @@
<template>
<div class="md:col-span-1 flex justify-between">
<div class="px-4 sm:px-0">
<h3 class="text-lg font-medium text-gray-900">
<slot name="title" />
</h3>
<p class="mt-1 text-sm text-gray-600">
<slot name="description" />
</p>
</div>
<div class="px-4 sm:px-0">
<slot name="aside" />
</div>
</div>
</template>

View File

@ -0,0 +1,28 @@
<script setup>
import { onMounted, ref } from 'vue';
defineProps({
modelValue: String,
});
defineEmits(['update:modelValue']);
const input = ref(null);
onMounted(() => {
if (input.value.hasAttribute('autofocus')) {
input.value.focus();
}
});
defineExpose({ focus: () => input.value.focus() });
</script>
<template>
<input
ref="input"
class="border-gray-300 focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50 rounded-md shadow-sm"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
</template>

View File

@ -0,0 +1,138 @@
<script setup>
import ApplicationLogo from '@/Components/ApplicationLogo.vue';
</script>
<template>
<div>
<div class="p-6 sm:px-20 bg-white border-b border-gray-200">
<div>
<ApplicationLogo class="block h-12 w-auto" />
</div>
<div class="mt-8 text-2xl">
Welcome to your Jetstream application!
</div>
<div class="mt-6 text-gray-500">
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.
</div>
</div>
<div class="bg-gray-200 bg-opacity-25 grid grid-cols-1 md:grid-cols-2">
<div class="p-6">
<div class="flex items-center">
<svg
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
class="w-8 h-8 text-gray-400"
><path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /></svg>
<div class="ml-4 text-lg text-gray-600 leading-7 font-semibold">
<a href="https://laravel.com/docs">Documentation</a>
</div>
</div>
<div class="ml-12">
<div class="mt-2 text-sm text-gray-500">
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.
</div>
<a href="https://laravel.com/docs">
<div class="mt-3 flex items-center text-sm font-semibold text-indigo-700">
<div>Explore the documentation</div>
<div class="ml-1 text-indigo-500">
<svg viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
</div>
</div>
</a>
</div>
</div>
<div class="p-6 border-t border-gray-200 md:border-t-0 md:border-l">
<div class="flex items-center">
<svg
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
class="w-8 h-8 text-gray-400"
><path d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" /><path d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
<div class="ml-4 text-lg text-gray-600 leading-7 font-semibold">
<a href="https://laracasts.com">Laracasts</a>
</div>
</div>
<div class="ml-12">
<div class="mt-2 text-sm text-gray-500">
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.
</div>
<a href="https://laracasts.com">
<div class="mt-3 flex items-center text-sm font-semibold text-indigo-700">
<div>Start watching Laracasts</div>
<div class="ml-1 text-indigo-500">
<svg viewBox="0 0 20 20" fill="currentColor" class="w-4 h-4"><path fill-rule="evenodd" d="M10.293 3.293a1 1 0 011.414 0l6 6a1 1 0 010 1.414l-6 6a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-4.293-4.293a1 1 0 010-1.414z" clip-rule="evenodd" /></svg>
</div>
</div>
</a>
</div>
</div>
<div class="p-6 border-t border-gray-200">
<div class="flex items-center">
<svg
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
class="w-8 h-8 text-gray-400"
><path d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
<div class="ml-4 text-lg text-gray-600 leading-7 font-semibold">
<a href="https://tailwindcss.com/">Tailwind</a>
</div>
</div>
<div class="ml-12">
<div class="mt-2 text-sm text-gray-500">
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.
</div>
</div>
</div>
<div class="p-6 border-t border-gray-200 md:border-l">
<div class="flex items-center">
<svg
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
class="w-8 h-8 text-gray-400"
><path d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
<div class="ml-4 text-lg text-gray-600 leading-7 font-semibold">
Authentication
</div>
</div>
<div class="ml-12">
<div class="mt-2 text-sm text-gray-500">
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 what matters most: building your application.
</div>
</div>
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,311 @@
<script setup>
import { ref } from 'vue';
import { Inertia } from '@inertiajs/inertia';
import { Head, Link } from '@inertiajs/inertia-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';
defineProps({
title: String,
});
const showingNavigationDropdown = ref(false);
const switchToTeam = (team) => {
Inertia.put(route('current-team.update'), {
team_id: team.id,
}, {
preserveState: false,
});
};
const logout = () => {
Inertia.post(route('logout'));
};
</script>
<template>
<div>
<Head :title="title" />
<Banner />
<div class="min-h-screen bg-gray-100">
<nav class="bg-white border-b border-gray-100">
<!-- 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">
<div class="flex">
<!-- Logo -->
<div class="shrink-0 flex items-center">
<Link :href="route('dashboard')">
<ApplicationMark class="block h-9 w-auto" />
</Link>
</div>
<!-- Navigation Links -->
<div class="hidden space-x-8 sm:-my-px sm:ml-10 sm:flex">
<NavLink :href="route('dashboard')" :active="route().current('dashboard')">
Dashboard
</NavLink>
</div>
</div>
<div class="hidden sm:flex sm:items-center sm:ml-6">
<div class="ml-3 relative">
<!-- Teams Dropdown -->
<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 bg-white hover:bg-gray-50 hover:text-gray-700 focus:outline-none focus:bg-gray-50 active:bg-gray-50 transition">
{{ $page.props.user.current_team.name }}
<svg
class="ml-2 -mr-0.5 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path fill-rule="evenodd" d="M10 3a1 1 0 01.707.293l3 3a1 1 0 01-1.414 1.414L10 5.414 7.707 7.707a1 1 0 01-1.414-1.414l3-3A1 1 0 0110 3zm-3.707 9.293a1 1 0 011.414 0L10 14.586l2.293-2.293a1 1 0 011.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</span>
</template>
<template #content>
<div class="w-60">
<!-- Team Management -->
<template v-if="$page.props.jetstream.hasTeamFeatures">
<div class="block px-4 py-2 text-xs text-gray-400">
Manage Team
</div>
<!-- Team Settings -->
<DropdownLink :href="route('teams.show', $page.props.user.current_team)">
Team Settings
</DropdownLink>
<DropdownLink v-if="$page.props.jetstream.canCreateTeams" :href="route('teams.create')">
Create New Team
</DropdownLink>
<div class="border-t border-gray-100" />
<!-- Team Switcher -->
<div class="block px-4 py-2 text-xs text-gray-400">
Switch Teams
</div>
<template v-for="team in $page.props.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.user.current_team_id"
class="mr-2 h-5 w-5 text-green-400"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
stroke="currentColor"
viewBox="0 0 24 24"
><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<div>{{ team.name }}</div>
</div>
</DropdownLink>
</form>
</template>
</template>
</div>
</template>
</Dropdown>
</div>
<!-- Settings Dropdown -->
<div class="ml-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.user.profile_photo_url" :alt="$page.props.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 bg-white hover:text-gray-700 focus:outline-none transition">
{{ $page.props.user.name }}
<svg
class="ml-2 -mr-0.5 h-4 w-4"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
>
<path fill-rule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clip-rule="evenodd" />
</svg>
</button>
</span>
</template>
<template #content>
<!-- Account Management -->
<div class="block px-4 py-2 text-xs text-gray-400">
Manage Account
</div>
<DropdownLink :href="route('profile.show')">
Profile
</DropdownLink>
<DropdownLink v-if="$page.props.jetstream.hasApiFeatures" :href="route('api-tokens.index')">
API Tokens
</DropdownLink>
<div class="border-t border-gray-100" />
<!-- Authentication -->
<form @submit.prevent="logout">
<DropdownLink as="button">
Log Out
</DropdownLink>
</form>
</template>
</Dropdown>
</div>
</div>
<!-- Hamburger -->
<div class="-mr-2 flex items-center sm:hidden">
<button class="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:bg-gray-100 focus:text-gray-500 transition" @click="showingNavigationDropdown = ! showingNavigationDropdown">
<svg
class="h-6 w-6"
stroke="currentColor"
fill="none"
viewBox="0 0 24 24"
>
<path
:class="{'hidden': showingNavigationDropdown, 'inline-flex': ! showingNavigationDropdown }"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
/>
<path
:class="{'hidden': ! showingNavigationDropdown, 'inline-flex': showingNavigationDropdown }"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
</div>
<!-- Responsive Navigation Menu -->
<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')">
Dashboard
</ResponsiveNavLink>
</div>
<!-- Responsive Settings Options -->
<div class="pt-4 pb-1 border-t border-gray-200">
<div class="flex items-center px-4">
<div v-if="$page.props.jetstream.managesProfilePhotos" class="shrink-0 mr-3">
<img class="h-10 w-10 rounded-full object-cover" :src="$page.props.user.profile_photo_url" :alt="$page.props.user.name">
</div>
<div>
<div class="font-medium text-base text-gray-800">
{{ $page.props.user.name }}
</div>
<div class="font-medium text-sm text-gray-500">
{{ $page.props.user.email }}
</div>
</div>
</div>
<div class="mt-3 space-y-1">
<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')">
API Tokens
</ResponsiveNavLink>
<!-- Authentication -->
<form method="POST" @submit.prevent="logout">
<ResponsiveNavLink as="button">
Log Out
</ResponsiveNavLink>
</form>
<!-- Team Management -->
<template v-if="$page.props.jetstream.hasTeamFeatures">
<div class="border-t border-gray-200" />
<div class="block px-4 py-2 text-xs text-gray-400">
Manage Team
</div>
<!-- Team Settings -->
<ResponsiveNavLink :href="route('teams.show', $page.props.user.current_team)" :active="route().current('teams.show')">
Team Settings
</ResponsiveNavLink>
<ResponsiveNavLink v-if="$page.props.jetstream.canCreateTeams" :href="route('teams.create')" :active="route().current('teams.create')">
Create New Team
</ResponsiveNavLink>
<div class="border-t border-gray-200" />
<!-- Team Switcher -->
<div class="block px-4 py-2 text-xs text-gray-400">
Switch Teams
</div>
<template v-for="team in $page.props.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.user.current_team_id"
class="mr-2 h-5 w-5 text-green-400"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
stroke="currentColor"
viewBox="0 0 24 24"
><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<div>{{ team.name }}</div>
</div>
</ResponsiveNavLink>
</form>
</template>
</template>
</div>
</div>
</div>
</nav>
<!-- Page Heading -->
<header v-if="$slots.header" class="bg-white shadow">
<div class="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<slot name="header" />
</div>
</header>
<!-- Page Content -->
<main>
<slot />
</main>
</div>
</div>
</template>

View File

@ -0,0 +1,30 @@
<script setup>
import ApiTokenManager from '@/Pages/API/Partials/ApiTokenManager.vue';
import AppLayout from '@/Layouts/AppLayout.vue';
defineProps({
tokens: Array,
availablePermissions: Array,
defaultPermissions: Array,
});
</script>
<template>
<AppLayout title="API Tokens">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
API Tokens
</h2>
</template>
<div>
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
<ApiTokenManager
:tokens="tokens"
:available-permissions="availablePermissions"
:default-permissions="defaultPermissions"
/>
</div>
</div>
</AppLayout>
</template>

View File

@ -0,0 +1,255 @@
<script setup>
import { ref } from 'vue';
import { useForm } from '@inertiajs/inertia-vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import ActionSection from '@/Components/ActionSection.vue';
import Checkbox from '@/Components/Checkbox.vue';
import ConfirmationModal from '@/Components/ConfirmationModal.vue';
import DangerButton from '@/Components/DangerButton.vue';
import DialogModal from '@/Components/DialogModal.vue';
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 SecondaryButton from '@/Components/SecondaryButton.vue';
import SectionBorder from '@/Components/SectionBorder.vue';
import TextInput from '@/Components/TextInput.vue';
const props = defineProps({
tokens: Array,
availablePermissions: Array,
defaultPermissions: Array,
});
const createApiTokenForm = useForm({
name: '',
permissions: props.defaultPermissions,
});
const updateApiTokenForm = useForm({
permissions: [],
});
const deleteApiTokenForm = useForm();
const displayingToken = ref(false);
const managingPermissionsFor = ref(null);
const apiTokenBeingDeleted = ref(null);
const createApiToken = () => {
createApiTokenForm.post(route('api-tokens.store'), {
preserveScroll: true,
onSuccess: () => {
displayingToken.value = true;
createApiTokenForm.reset();
},
});
};
const manageApiTokenPermissions = (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),
});
};
const confirmApiTokenDeletion = (token) => {
apiTokenBeingDeleted.value = token;
};
const deleteApiToken = () => {
deleteApiTokenForm.delete(route('api-tokens.destroy', apiTokenBeingDeleted.value), {
preserveScroll: true,
preserveState: true,
onSuccess: () => (apiTokenBeingDeleted.value = null),
});
};
</script>
<template>
<div>
<!-- Generate API Token -->
<FormSection @submitted="createApiToken">
<template #title>
Create API Token
</template>
<template #description>
API tokens allow third-party services to authenticate with our application on your behalf.
</template>
<template #form>
<!-- Token Name -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Name" />
<TextInput
id="name"
v-model="createApiTokenForm.name"
type="text"
class="mt-1 block w-full"
autofocus
/>
<InputError :message="createApiTokenForm.errors.name" class="mt-2" />
</div>
<!-- Token Permissions -->
<div v-if="availablePermissions.length > 0" class="col-span-6">
<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">
<label class="flex items-center">
<Checkbox v-model:checked="createApiTokenForm.permissions" :value="permission" />
<span class="ml-2 text-sm text-gray-600">{{ permission }}</span>
</label>
</div>
</div>
</div>
</template>
<template #actions>
<ActionMessage :on="createApiTokenForm.recentlySuccessful" class="mr-3">
Created.
</ActionMessage>
<PrimaryButton :class="{ 'opacity-25': createApiTokenForm.processing }" :disabled="createApiTokenForm.processing">
Create
</PrimaryButton>
</template>
</FormSection>
<div v-if="tokens.length > 0">
<SectionBorder />
<!-- Manage API Tokens -->
<div class="mt-10 sm:mt-0">
<ActionSection>
<template #title>
Manage API Tokens
</template>
<template #description>
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>
{{ token.name }}
</div>
<div class="flex items-center">
<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 ml-6 text-sm text-gray-400 underline"
@click="manageApiTokenPermissions(token)"
>
Permissions
</button>
<button class="cursor-pointer ml-6 text-sm text-red-500" @click="confirmApiTokenDeletion(token)">
Delete
</button>
</div>
</div>
</div>
</template>
</ActionSection>
</div>
</div>
<!-- Token Value Modal -->
<DialogModal :show="displayingToken" @close="displayingToken = false">
<template #title>
API Token
</template>
<template #content>
<div>
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 px-4 py-2 rounded font-mono text-sm text-gray-500">
{{ $page.props.jetstream.flash.token }}
</div>
</template>
<template #footer>
<SecondaryButton @click="displayingToken = false">
Close
</SecondaryButton>
</template>
</DialogModal>
<!-- API Token Permissions Modal -->
<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">
<label class="flex items-center">
<Checkbox v-model:checked="updateApiTokenForm.permissions" :value="permission" />
<span class="ml-2 text-sm text-gray-600">{{ permission }}</span>
</label>
</div>
</div>
</template>
<template #footer>
<SecondaryButton @click="managingPermissionsFor = null">
Cancel
</SecondaryButton>
<PrimaryButton
class="ml-3"
:class="{ 'opacity-25': updateApiTokenForm.processing }"
:disabled="updateApiTokenForm.processing"
@click="updateApiToken"
>
Save
</PrimaryButton>
</template>
</DialogModal>
<!-- Delete Token Confirmation Modal -->
<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?
</template>
<template #footer>
<SecondaryButton @click="apiTokenBeingDeleted = null">
Cancel
</SecondaryButton>
<DangerButton
class="ml-3"
:class="{ 'opacity-25': deleteApiTokenForm.processing }"
:disabled="deleteApiTokenForm.processing"
@click="deleteApiToken"
>
Delete
</DangerButton>
</template>
</ConfirmationModal>
</div>
</template>

View File

@ -0,0 +1,63 @@
<script setup>
import { ref } from 'vue';
import { Head, useForm } from '@inertiajs/inertia-vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.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';
const form = useForm({
password: '',
});
const passwordInput = ref(null);
const submit = () => {
form.post(route('password.confirm'), {
onFinish: () => {
form.reset();
passwordInput.value.focus();
},
});
};
</script>
<template>
<Head title="Secure Area" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<div class="mb-4 text-sm text-gray-600">
This is a secure area of the application. Please confirm your password before continuing.
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="password" value="Password" />
<TextInput
id="password"
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-full"
required
autocomplete="current-password"
autofocus
/>
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="flex justify-end mt-4">
<PrimaryButton class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Confirm
</PrimaryButton>
</div>
</form>
</AuthenticationCard>
</template>

View File

@ -0,0 +1,60 @@
<script setup>
import { Head, useForm } from '@inertiajs/inertia-vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.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';
defineProps({
status: String,
});
const form = useForm({
email: '',
});
const submit = () => {
form.post(route('password.email'));
};
</script>
<template>
<Head title="Forgot Password" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<div class="mb-4 text-sm text-gray-600">
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">
{{ status }}
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="email" value="Email" />
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
required
autofocus
/>
<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">
Email Password Reset Link
</PrimaryButton>
</div>
</form>
</AuthenticationCard>
</template>

View File

@ -0,0 +1,89 @@
<script setup>
import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import Checkbox from '@/Components/Checkbox.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';
defineProps({
canResetPassword: Boolean,
status: String,
});
const form = useForm({
email: '',
password: '',
remember: false,
});
const submit = () => {
form.transform(data => ({
...data,
remember: form.remember ? 'on' : '',
})).post(route('login'), {
onFinish: () => form.reset('password'),
});
};
</script>
<template>
<Head title="Log in" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<div v-if="status" class="mb-4 font-medium text-sm text-green-600">
{{ status }}
</div>
<form @submit.prevent="submit">
<div>
<InputLabel for="email" value="Email" />
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
required
autofocus
/>
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div class="mt-4">
<InputLabel for="password" value="Password" />
<TextInput
id="password"
v-model="form.password"
type="password"
class="mt-1 block w-full"
required
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="ml-2 text-sm text-gray-600">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 hover:text-gray-900">
Forgot your password?
</Link>
<PrimaryButton class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Log in
</PrimaryButton>
</div>
</form>
</AuthenticationCard>
</template>

View File

@ -0,0 +1,111 @@
<script setup>
import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import Checkbox from '@/Components/Checkbox.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';
const form = useForm({
name: '',
email: '',
password: '',
password_confirmation: '',
terms: false,
});
const submit = () => {
form.post(route('register'), {
onFinish: () => form.reset('password', 'password_confirmation'),
});
};
</script>
<template>
<Head title="Register" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<form @submit.prevent="submit">
<div>
<InputLabel for="name" value="Name" />
<TextInput
id="name"
v-model="form.name"
type="text"
class="mt-1 block w-full"
required
autofocus
autocomplete="name"
/>
<InputError class="mt-2" :message="form.errors.name" />
</div>
<div class="mt-4">
<InputLabel for="email" value="Email" />
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
required
/>
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div class="mt-4">
<InputLabel for="password" value="Password" />
<TextInput
id="password"
v-model="form.password"
type="password"
class="mt-1 block w-full"
required
autocomplete="new-password"
/>
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="mt-4">
<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" />
</div>
<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 />
<div class="ml-2">
I agree to the <a target="_blank" :href="route('terms.show')" class="underline text-sm text-gray-600 hover:text-gray-900">Terms of Service</a> and <a target="_blank" :href="route('policy.show')" class="underline text-sm text-gray-600 hover:text-gray-900">Privacy Policy</a>
</div>
</div>
<InputError class="mt-2" :message="form.errors.terms" />
</InputLabel>
</div>
<div class="flex items-center justify-end mt-4">
<Link :href="route('login')" class="underline text-sm text-gray-600 hover:text-gray-900">
Already registered?
</Link>
<PrimaryButton class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Register
</PrimaryButton>
</div>
</form>
</AuthenticationCard>
</template>

View File

@ -0,0 +1,84 @@
<script setup>
import { Head, useForm } from '@inertiajs/inertia-vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.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';
const props = defineProps({
email: String,
token: String,
});
const form = useForm({
token: props.token,
email: props.email,
password: '',
password_confirmation: '',
});
const submit = () => {
form.post(route('password.update'), {
onFinish: () => form.reset('password', 'password_confirmation'),
});
};
</script>
<template>
<Head title="Reset Password" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<form @submit.prevent="submit">
<div>
<InputLabel for="email" value="Email" />
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
required
autofocus
/>
<InputError class="mt-2" :message="form.errors.email" />
</div>
<div class="mt-4">
<InputLabel for="password" value="Password" />
<TextInput
id="password"
v-model="form.password"
type="password"
class="mt-1 block w-full"
required
autocomplete="new-password"
/>
<InputError class="mt-2" :message="form.errors.password" />
</div>
<div class="mt-4">
<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" />
</div>
<div class="flex items-center justify-end mt-4">
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Reset Password
</PrimaryButton>
</div>
</form>
</AuthenticationCard>
</template>

View File

@ -0,0 +1,104 @@
<script setup>
import { nextTick, ref } from 'vue';
import { Head, useForm } from '@inertiajs/inertia-vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.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';
const recovery = ref(false);
const form = useForm({
code: '',
recovery_code: '',
});
const recoveryCodeInput = ref(null);
const codeInput = ref(null);
const toggleRecovery = async () => {
recovery.value ^= true;
await nextTick();
if (recovery.value) {
recoveryCodeInput.value.focus();
form.code = '';
} else {
codeInput.value.focus();
form.recovery_code = '';
}
};
const submit = () => {
form.post(route('two-factor.login'));
};
</script>
<template>
<Head title="Two-factor Confirmation" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<div class="mb-4 text-sm text-gray-600">
<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.
</template>
</div>
<form @submit.prevent="submit">
<div v-if="! recovery">
<InputLabel for="code" value="Code" />
<TextInput
id="code"
ref="codeInput"
v-model="form.code"
type="text"
inputmode="numeric"
class="mt-1 block w-full"
autofocus
autocomplete="one-time-code"
/>
<InputError class="mt-2" :message="form.errors.code" />
</div>
<div v-else>
<InputLabel for="recovery_code" value="Recovery Code" />
<TextInput
id="recovery_code"
ref="recoveryCodeInput"
v-model="form.recovery_code"
type="text"
class="mt-1 block w-full"
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 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>
</button>
<PrimaryButton class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Log in
</PrimaryButton>
</div>
</form>
</AuthenticationCard>
</template>

View File

@ -0,0 +1,62 @@
<script setup>
import { computed } from 'vue';
import { Head, Link, useForm } from '@inertiajs/inertia-vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
const props = defineProps({
status: String,
});
const form = useForm();
const submit = () => {
form.post(route('verification.send'));
};
const verificationLinkSent = computed(() => props.status === 'verification-link-sent');
</script>
<template>
<Head title="Email Verification" />
<AuthenticationCard>
<template #logo>
<AuthenticationCardLogo />
</template>
<div class="mb-4 text-sm text-gray-600">
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">
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">
Resend Verification Email
</PrimaryButton>
<div>
<Link
:href="route('profile.show')"
class="underline text-sm text-gray-600 hover:text-gray-900"
>
Edit Profile</Link>
<Link
:href="route('logout')"
method="post"
as="button"
class="underline text-sm text-gray-600 hover:text-gray-900 ml-2"
>
Log Out
</Link>
</div>
</div>
</form>
</AuthenticationCard>
</template>

View File

@ -0,0 +1,22 @@
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
import Welcome from '@/Components/Welcome.vue';
</script>
<template>
<AppLayout title="Dashboard">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 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 overflow-hidden shadow-xl sm:rounded-lg">
<Welcome />
</div>
</div>
</div>
</AppLayout>
</template>

View File

@ -0,0 +1,24 @@
<script setup>
import { Head } from '@inertiajs/inertia-vue3';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
defineProps({
policy: String,
});
</script>
<template>
<Head title="Privacy Policy" />
<div class="font-sans text-gray-900 antialiased">
<div class="pt-4 bg-gray-100">
<div class="min-h-screen flex flex-col items-center pt-6 sm:pt-0">
<div>
<AuthenticationCardLogo />
</div>
<div class="w-full sm:max-w-2xl mt-6 p-6 bg-white shadow-md overflow-hidden sm:rounded-lg prose" v-html="policy" />
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,101 @@
<script setup>
import { ref } from 'vue';
import { useForm } from '@inertiajs/inertia-vue3';
import ActionSection from '@/Components/ActionSection.vue';
import DangerButton from '@/Components/DangerButton.vue';
import DialogModal from '@/Components/DialogModal.vue';
import InputError from '@/Components/InputError.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
const confirmingUserDeletion = ref(false);
const passwordInput = ref(null);
const form = useForm({
password: '',
});
const confirmUserDeletion = () => {
confirmingUserDeletion.value = true;
setTimeout(() => passwordInput.value.focus(), 250);
};
const deleteUser = () => {
form.delete(route('current-user.destroy'), {
preserveScroll: true,
onSuccess: () => closeModal(),
onError: () => passwordInput.value.focus(),
onFinish: () => form.reset(),
});
};
const closeModal = () => {
confirmingUserDeletion.value = false;
form.reset();
};
</script>
<template>
<ActionSection>
<template #title>
Delete Account
</template>
<template #description>
Permanently delete your account.
</template>
<template #content>
<div class="max-w-xl text-sm text-gray-600">
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">
<DangerButton @click="confirmUserDeletion">
Delete Account
</DangerButton>
</div>
<!-- Delete Account Confirmation Modal -->
<DialogModal :show="confirmingUserDeletion" @close="closeModal">
<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.
<div class="mt-4">
<TextInput
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-3/4"
placeholder="Password"
@keyup.enter="deleteUser"
/>
<InputError :message="form.errors.password" class="mt-2" />
</div>
</template>
<template #footer>
<SecondaryButton @click="closeModal">
Cancel
</SecondaryButton>
<DangerButton
class="ml-3"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="deleteUser"
>
Delete Account
</DangerButton>
</template>
</DialogModal>
</template>
</ActionSection>
</template>

View File

@ -0,0 +1,165 @@
<script setup>
import { ref } from 'vue';
import { useForm } from '@inertiajs/inertia-vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import ActionSection from '@/Components/ActionSection.vue';
import DialogModal from '@/Components/DialogModal.vue';
import InputError from '@/Components/InputError.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
defineProps({
sessions: Array,
});
const confirmingLogout = ref(false);
const passwordInput = ref(null);
const form = useForm({
password: '',
});
const confirmLogout = () => {
confirmingLogout.value = true;
setTimeout(() => passwordInput.value.focus(), 250);
};
const logoutOtherBrowserSessions = () => {
form.delete(route('other-browser-sessions.destroy'), {
preserveScroll: true,
onSuccess: () => closeModal(),
onError: () => passwordInput.value.focus(),
onFinish: () => form.reset(),
});
};
const closeModal = () => {
confirmingLogout.value = false;
form.reset();
};
</script>
<template>
<ActionSection>
<template #title>
Browser Sessions
</template>
<template #description>
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">
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>
<svg
v-if="session.agent.is_desktop"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
stroke="currentColor"
class="w-8 h-8 text-gray-500"
>
<path d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<svg
v-else
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
class="w-8 h-8 text-gray-500"
>
<path d="M0 0h24v24H0z" stroke="none" /><rect
x="7"
y="4"
width="10"
height="16"
rx="1"
/><path d="M11 5h2M12 17v.01" />
</svg>
</div>
<div class="ml-3">
<div class="text-sm text-gray-600">
{{ 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>
</div>
</div>
</div>
</div>
</div>
<div class="flex items-center mt-5">
<PrimaryButton @click="confirmLogout">
Log Out Other Browser Sessions
</PrimaryButton>
<ActionMessage :on="form.recentlySuccessful" class="ml-3">
Done.
</ActionMessage>
</div>
<!-- Log Out Other Devices Confirmation Modal -->
<DialogModal :show="confirmingLogout" @close="closeModal">
<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.
<div class="mt-4">
<TextInput
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-3/4"
placeholder="Password"
@keyup.enter="logoutOtherBrowserSessions"
/>
<InputError :message="form.errors.password" class="mt-2" />
</div>
</template>
<template #footer>
<SecondaryButton @click="closeModal">
Cancel
</SecondaryButton>
<PrimaryButton
class="ml-3"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="logoutOtherBrowserSessions"
>
Log Out Other Browser Sessions
</PrimaryButton>
</template>
</DialogModal>
</template>
</ActionSection>
</template>

View File

@ -0,0 +1,253 @@
<script setup>
import { ref, computed, watch } from 'vue';
import { Inertia } from '@inertiajs/inertia';
import { useForm, usePage } from '@inertiajs/inertia-vue3';
import ActionSection from '@/Components/ActionSection.vue';
import ConfirmsPassword from '@/Components/ConfirmsPassword.vue';
import DangerButton from '@/Components/DangerButton.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
const props = defineProps({
requiresConfirmation: Boolean,
});
const enabling = ref(false);
const confirming = ref(false);
const disabling = ref(false);
const qrCode = ref(null);
const setupKey = ref(null);
const recoveryCodes = ref([]);
const confirmationForm = useForm({
code: '',
});
const twoFactorEnabled = computed(
() => ! enabling.value && usePage().props.value.user?.two_factor_enabled,
);
watch(twoFactorEnabled, () => {
if (! twoFactorEnabled.value) {
confirmationForm.reset();
confirmationForm.clearErrors();
}
});
const enableTwoFactorAuthentication = () => {
enabling.value = true;
Inertia.post('/user/two-factor-authentication', {}, {
preserveScroll: true,
onSuccess: () => Promise.all([
showQrCode(),
showSetupKey(),
showRecoveryCodes(),
]),
onFinish: () => {
enabling.value = false;
confirming.value = props.requiresConfirmation;
},
});
};
const showQrCode = () => {
return axios.get('/user/two-factor-qr-code').then(response => {
qrCode.value = response.data.svg;
});
};
const showSetupKey = () => {
return axios.get('/user/two-factor-secret-key').then(response => {
setupKey.value = response.data.secretKey;
});
}
const showRecoveryCodes = () => {
return axios.get('/user/two-factor-recovery-codes').then(response => {
recoveryCodes.value = response.data;
});
};
const confirmTwoFactorAuthentication = () => {
confirmationForm.post('/user/confirmed-two-factor-authentication', {
errorBag: "confirmTwoFactorAuthentication",
preserveScroll: true,
preserveState: true,
onSuccess: () => {
confirming.value = false;
qrCode.value = null;
setupKey.value = null;
},
});
};
const regenerateRecoveryCodes = () => {
axios
.post('/user/two-factor-recovery-codes')
.then(() => showRecoveryCodes());
};
const disableTwoFactorAuthentication = () => {
disabling.value = true;
Inertia.delete('/user/two-factor-authentication', {
preserveScroll: true,
onSuccess: () => {
disabling.value = false;
confirming.value = false;
},
});
};
</script>
<template>
<ActionSection>
<template #title>
Two Factor Authentication
</template>
<template #description>
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">
You have enabled two factor authentication.
</h3>
<h3 v-else-if="twoFactorEnabled && confirming" class="text-lg font-medium text-gray-900">
Finish enabling two factor authentication.
</h3>
<h3 v-else class="text-lg font-medium text-gray-900">
You have not enabled two factor authentication.
</h3>
<div class="mt-3 max-w-xl text-sm text-gray-600">
<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.
</p>
</div>
<div v-if="twoFactorEnabled">
<div v-if="qrCode">
<div class="mt-4 max-w-xl text-sm text-gray-600">
<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.
</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.
</p>
</div>
<div class="mt-4" v-html="qrCode" />
<div class="mt-4 max-w-xl text-sm text-gray-600" v-if="setupKey">
<p class="font-semibold">
Setup Key: <span v-html="setupKey"></span>
</p>
</div>
<div v-if="confirming" class="mt-4">
<InputLabel for="code" value="Code" />
<TextInput
id="code"
v-model="confirmationForm.code"
type="text"
name="code"
class="block mt-1 w-1/2"
inputmode="numeric"
autofocus
autocomplete="one-time-code"
@keyup.enter="confirmTwoFactorAuthentication"
/>
<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">
<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.
</p>
</div>
<div class="grid gap-1 max-w-xl mt-4 px-4 py-4 font-mono text-sm bg-gray-100 rounded-lg">
<div v-for="code in recoveryCodes" :key="code">
{{ code }}
</div>
</div>
</div>
</div>
<div class="mt-5">
<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">
<PrimaryButton
v-if="confirming"
type="button"
class="mr-3"
:class="{ 'opacity-25': enabling }"
:disabled="enabling"
>
Confirm
</PrimaryButton>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="regenerateRecoveryCodes">
<SecondaryButton
v-if="recoveryCodes.length > 0 && ! confirming"
class="mr-3"
>
Regenerate Recovery Codes
</SecondaryButton>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="showRecoveryCodes">
<SecondaryButton
v-if="recoveryCodes.length === 0 && ! confirming"
class="mr-3"
>
Show Recovery Codes
</SecondaryButton>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<SecondaryButton
v-if="confirming"
:class="{ 'opacity-25': disabling }"
:disabled="disabling"
>
Cancel
</SecondaryButton>
</ConfirmsPassword>
<ConfirmsPassword @confirmed="disableTwoFactorAuthentication">
<DangerButton
v-if="! confirming"
:class="{ 'opacity-25': disabling }"
:disabled="disabling"
>
Disable
</DangerButton>
</ConfirmsPassword>
</div>
</div>
</template>
</ActionSection>
</template>

View File

@ -0,0 +1,100 @@
<script setup>
import { ref } from 'vue';
import { useForm } from '@inertiajs/inertia-vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
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';
const passwordInput = ref(null);
const currentPasswordInput = ref(null);
const form = useForm({
current_password: '',
password: '',
password_confirmation: '',
});
const updatePassword = () => {
form.put(route('user-password.update'), {
errorBag: 'updatePassword',
preserveScroll: true,
onSuccess: () => form.reset(),
onError: () => {
if (form.errors.password) {
form.reset('password', 'password_confirmation');
passwordInput.value.focus();
}
if (form.errors.current_password) {
form.reset('current_password');
currentPasswordInput.value.focus();
}
},
});
};
</script>
<template>
<FormSection @submitted="updatePassword">
<template #title>
Update Password
</template>
<template #description>
Ensure your account is using a long, random password to stay secure.
</template>
<template #form>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="current_password" value="Current Password" />
<TextInput
id="current_password"
ref="currentPasswordInput"
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" />
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="password" value="New Password" />
<TextInput
id="password"
ref="passwordInput"
v-model="form.password"
type="password"
class="mt-1 block w-full"
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" />
<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" />
</div>
</template>
<template #actions>
<ActionMessage :on="form.recentlySuccessful" class="mr-3">
Saved.
</ActionMessage>
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Save
</PrimaryButton>
</template>
</FormSection>
</template>

View File

@ -0,0 +1,187 @@
<script setup>
import { ref } from 'vue';
import { Inertia } from '@inertiajs/inertia';
import { Link, useForm } from '@inertiajs/inertia-vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
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 SecondaryButton from '@/Components/SecondaryButton.vue';
import TextInput from '@/Components/TextInput.vue';
const props = defineProps({
user: Object,
});
const form = useForm({
_method: 'PUT',
name: props.user.name,
email: props.user.email,
photo: null,
});
const verificationLinkSent = ref(null);
const photoPreview = ref(null);
const photoInput = ref(null);
const updateProfileInformation = () => {
if (photoInput.value) {
form.photo = photoInput.value.files[0];
}
form.post(route('user-profile-information.update'), {
errorBag: 'updateProfileInformation',
preserveScroll: true,
onSuccess: () => clearPhotoFileInput(),
});
};
const sendEmailVerification = () => {
verificationLinkSent.value = true;
};
const selectNewPhoto = () => {
photoInput.value.click();
};
const updatePhotoPreview = () => {
const photo = photoInput.value.files[0];
if (! photo) return;
const reader = new FileReader();
reader.onload = (e) => {
photoPreview.value = e.target.result;
};
reader.readAsDataURL(photo);
};
const deletePhoto = () => {
Inertia.delete(route('current-user-photo.destroy'), {
preserveScroll: true,
onSuccess: () => {
photoPreview.value = null;
clearPhotoFileInput();
},
});
};
const clearPhotoFileInput = () => {
if (photoInput.value?.value) {
photoInput.value.value = null;
}
};
</script>
<template>
<FormSection @submitted="updateProfileInformation">
<template #title>
Profile Information
</template>
<template #description>
Update your account's profile information and email address.
</template>
<template #form>
<!-- Profile Photo -->
<div v-if="$page.props.jetstream.managesProfilePhotos" class="col-span-6 sm:col-span-4">
<!-- Profile Photo File Input -->
<input
ref="photoInput"
type="file"
class="hidden"
@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>
<!-- 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 + '\');'"
/>
</div>
<SecondaryButton class="mt-2 mr-2" type="button" @click.prevent="selectNewPhoto">
Select A New Photo
</SecondaryButton>
<SecondaryButton
v-if="user.profile_photo_path"
type="button"
class="mt-2"
@click.prevent="deletePhoto"
>
Remove Photo
</SecondaryButton>
<InputError :message="form.errors.photo" class="mt-2" />
</div>
<!-- Name -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Name" />
<TextInput
id="name"
v-model="form.name"
type="text"
class="mt-1 block w-full"
autocomplete="name"
/>
<InputError :message="form.errors.name" class="mt-2" />
</div>
<!-- Email -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="email" value="Email" />
<TextInput
id="email"
v-model="form.email"
type="email"
class="mt-1 block w-full"
/>
<InputError :message="form.errors.email" class="mt-2" />
<div v-if="$page.props.jetstream.hasEmailVerification && user.email_verified_at === null">
<p class="text-sm mt-2">
Your email address is unverified.
<Link
:href="route('verification.send')"
method="post"
as="button"
class="underline text-gray-600 hover:text-gray-900"
@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">
A new verification link has been sent to your email address.
</div>
</div>
</div>
</template>
<template #actions>
<ActionMessage :on="form.recentlySuccessful" class="mr-3">
Saved.
</ActionMessage>
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Save
</PrimaryButton>
</template>
</FormSection>
</template>

View File

@ -0,0 +1,57 @@
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
import DeleteUserForm from '@/Pages/Profile/Partials/DeleteUserForm.vue';
import LogoutOtherBrowserSessionsForm from '@/Pages/Profile/Partials/LogoutOtherBrowserSessionsForm.vue';
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';
defineProps({
confirmsTwoFactorAuthentication: Boolean,
sessions: Array,
});
</script>
<template>
<AppLayout title="Profile">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 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.user" />
<SectionBorder />
</div>
<div v-if="$page.props.jetstream.canUpdatePassword">
<UpdatePasswordForm class="mt-10 sm:mt-0" />
<SectionBorder />
</div>
<div v-if="$page.props.jetstream.canManageTwoFactorAuthentication">
<TwoFactorAuthenticationForm
:requires-confirmation="confirmsTwoFactorAuthentication"
class="mt-10 sm:mt-0"
/>
<SectionBorder />
</div>
<LogoutOtherBrowserSessionsForm :sessions="sessions" class="mt-10 sm:mt-0" />
<template v-if="$page.props.jetstream.hasAccountDeletionFeatures">
<SectionBorder />
<DeleteUserForm class="mt-10 sm:mt-0" />
</template>
</div>
</div>
</AppLayout>
</template>

View File

@ -0,0 +1,20 @@
<script setup>
import AppLayout from '@/Layouts/AppLayout.vue';
import CreateTeamForm from '@/Pages/Teams/Partials/CreateTeamForm.vue';
</script>
<template>
<AppLayout title="Create Team">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
Create Team
</h2>
</template>
<div>
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
<CreateTeamForm />
</div>
</div>
</AppLayout>
</template>

View File

@ -0,0 +1,66 @@
<script setup>
import { useForm } from '@inertiajs/inertia-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';
const form = useForm({
name: '',
});
const createTeam = () => {
form.post(route('teams.store'), {
errorBag: 'createTeam',
preserveScroll: true,
});
};
</script>
<template>
<FormSection @submitted="createTeam">
<template #title>
Team Details
</template>
<template #description>
Create a new team to collaborate with others on projects.
</template>
<template #form>
<div class="col-span-6">
<InputLabel value="Team Owner" />
<div class="flex items-center mt-2">
<img class="object-cover w-12 h-12 rounded-full" :src="$page.props.user.profile_photo_url" :alt="$page.props.user.name">
<div class="ml-4 leading-tight">
<div>{{ $page.props.user.name }}</div>
<div class="text-sm text-gray-700">
{{ $page.props.user.email }}
</div>
</div>
</div>
</div>
<div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Team Name" />
<TextInput
id="name"
v-model="form.name"
type="text"
class="block w-full mt-1"
autofocus
/>
<InputError :message="form.errors.name" class="mt-2" />
</div>
</template>
<template #actions>
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Create
</PrimaryButton>
</template>
</FormSection>
</template>

View File

@ -0,0 +1,75 @@
<script setup>
import { ref } from 'vue';
import { useForm } from '@inertiajs/inertia-vue3';
import ActionSection from '@/Components/ActionSection.vue';
import ConfirmationModal from '@/Components/ConfirmationModal.vue';
import DangerButton from '@/Components/DangerButton.vue';
import SecondaryButton from '@/Components/SecondaryButton.vue';
const props = defineProps({
team: Object,
});
const confirmingTeamDeletion = ref(false);
const form = useForm();
const confirmTeamDeletion = () => {
confirmingTeamDeletion.value = true;
};
const deleteTeam = () => {
form.delete(route('teams.destroy', props.team), {
errorBag: 'deleteTeam',
});
};
</script>
<template>
<ActionSection>
<template #title>
Delete Team
</template>
<template #description>
Permanently delete this team.
</template>
<template #content>
<div class="max-w-xl text-sm text-gray-600">
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">
<DangerButton @click="confirmTeamDeletion">
Delete Team
</DangerButton>
</div>
<!-- Delete Team Confirmation Modal -->
<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.
</template>
<template #footer>
<SecondaryButton @click="confirmingTeamDeletion = false">
Cancel
</SecondaryButton>
<DangerButton
class="ml-3"
:class="{ 'opacity-25': form.processing }"
:disabled="form.processing"
@click="deleteTeam"
>
Delete Team
</DangerButton>
</template>
</ConfirmationModal>
</template>
</ActionSection>
</template>

View File

@ -0,0 +1,396 @@
<script setup>
import { ref } from 'vue';
import { Inertia } from '@inertiajs/inertia';
import { useForm, usePage } from '@inertiajs/inertia-vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
import ActionSection from '@/Components/ActionSection.vue';
import ConfirmationModal from '@/Components/ConfirmationModal.vue';
import DangerButton from '@/Components/DangerButton.vue';
import DialogModal from '@/Components/DialogModal.vue';
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 SecondaryButton from '@/Components/SecondaryButton.vue';
import SectionBorder from '@/Components/SectionBorder.vue';
import TextInput from '@/Components/TextInput.vue';
const props = defineProps({
team: Object,
availableRoles: Array,
userPermissions: Object,
});
const addTeamMemberForm = useForm({
email: '',
role: null,
});
const updateRoleForm = useForm({
role: null,
});
const leaveTeamForm = useForm();
const removeTeamMemberForm = useForm();
const currentlyManagingRole = ref(false);
const managingRoleFor = ref(null);
const confirmingLeavingTeam = ref(false);
const teamMemberBeingRemoved = ref(null);
const addTeamMember = () => {
addTeamMemberForm.post(route('team-members.store', props.team), {
errorBag: 'addTeamMember',
preserveScroll: true,
onSuccess: () => addTeamMemberForm.reset(),
});
};
const cancelTeamInvitation = (invitation) => {
Inertia.delete(route('team-invitations.destroy', invitation), {
preserveScroll: true,
});
};
const manageRole = (teamMember) => {
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,
});
};
const confirmLeavingTeam = () => {
confirmingLeavingTeam.value = true;
};
const leaveTeam = () => {
leaveTeamForm.delete(route('team-members.destroy', [props.team, usePage().props.value.user]));
};
const confirmTeamMemberRemoval = (teamMember) => {
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,
});
};
const displayableRole = (role) => {
return props.availableRoles.find(r => r.key === role).name;
};
</script>
<template>
<div>
<div v-if="userPermissions.canAddTeamMembers">
<SectionBorder />
<!-- Add Team Member -->
<FormSection @submitted="addTeamMember">
<template #title>
Add Team Member
</template>
<template #description>
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">
Please provide the email address of the person you would like to add to this team.
</div>
</div>
<!-- Member Email -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="email" value="Email" />
<TextInput
id="email"
v-model="addTeamMemberForm.email"
type="email"
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">
<InputLabel for="roles" value="Role" />
<InputError :message="addTeamMemberForm.errors.role" class="mt-2" />
<div class="relative z-0 mt-1 border border-gray-200 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-blue-300 focus:ring focus:ring-blue-200"
:class="{'border-t border-gray-200 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" :class="{'font-semibold': addTeamMemberForm.role == role.key}">
{{ role.name }}
</div>
<svg
v-if="addTeamMemberForm.role == role.key"
class="ml-2 h-5 w-5 text-green-400"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
stroke="currentColor"
viewBox="0 0 24 24"
><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
</div>
<!-- Role Description -->
<div class="mt-2 text-xs text-gray-600 text-left">
{{ role.description }}
</div>
</div>
</button>
</div>
</div>
</template>
<template #actions>
<ActionMessage :on="addTeamMemberForm.recentlySuccessful" class="mr-3">
Added.
</ActionMessage>
<PrimaryButton :class="{ 'opacity-25': addTeamMemberForm.processing }" :disabled="addTeamMemberForm.processing">
Add
</PrimaryButton>
</template>
</FormSection>
</div>
<div v-if="team.team_invitations.length > 0 && userPermissions.canAddTeamMembers">
<SectionBorder />
<!-- Team Member Invitations -->
<ActionSection class="mt-10 sm:mt-0">
<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.
</template>
<!-- Pending Team 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 class="text-gray-600">
{{ invitation.email }}
</div>
<div class="flex items-center">
<!-- Cancel Team Invitation -->
<button
v-if="userPermissions.canRemoveTeamMembers"
class="cursor-pointer ml-6 text-sm text-red-500 focus:outline-none"
@click="cancelTeamInvitation(invitation)"
>
Cancel
</button>
</div>
</div>
</div>
</template>
</ActionSection>
</div>
<div v-if="team.users.length > 0">
<SectionBorder />
<!-- Manage Team Members -->
<ActionSection class="mt-10 sm:mt-0">
<template #title>
Team Members
</template>
<template #description>
All of the people that are part of this team.
</template>
<!-- Team 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 class="flex items-center">
<img class="w-8 h-8 rounded-full" :src="user.profile_photo_url" :alt="user.name">
<div class="ml-4">
{{ user.name }}
</div>
</div>
<div class="flex items-center">
<!-- Manage Team Member Role -->
<button
v-if="userPermissions.canAddTeamMembers && availableRoles.length"
class="ml-2 text-sm text-gray-400 underline"
@click="manageRole(user)"
>
{{ displayableRole(user.membership.role) }}
</button>
<div v-else-if="availableRoles.length" class="ml-2 text-sm text-gray-400">
{{ displayableRole(user.membership.role) }}
</div>
<!-- Leave Team -->
<button
v-if="$page.props.user.id === user.id"
class="cursor-pointer ml-6 text-sm text-red-500"
@click="confirmLeavingTeam"
>
Leave
</button>
<!-- Remove Team Member -->
<button
v-else-if="userPermissions.canRemoveTeamMembers"
class="cursor-pointer ml-6 text-sm text-red-500"
@click="confirmTeamMemberRemoval(user)"
>
Remove
</button>
</div>
</div>
</div>
</template>
</ActionSection>
</div>
<!-- Role Management Modal -->
<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 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-blue-300 focus:ring focus:ring-blue-200"
:class="{'border-t border-gray-200 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" :class="{'font-semibold': updateRoleForm.role === role.key}">
{{ role.name }}
</div>
<svg
v-if="updateRoleForm.role === role.key"
class="ml-2 h-5 w-5 text-green-400"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
stroke="currentColor"
viewBox="0 0 24 24"
><path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
</div>
<!-- Role Description -->
<div class="mt-2 text-xs text-gray-600">
{{ role.description }}
</div>
</div>
</button>
</div>
</div>
</template>
<template #footer>
<SecondaryButton @click="currentlyManagingRole = false">
Cancel
</SecondaryButton>
<PrimaryButton
class="ml-3"
:class="{ 'opacity-25': updateRoleForm.processing }"
:disabled="updateRoleForm.processing"
@click="updateRole"
>
Save
</PrimaryButton>
</template>
</DialogModal>
<!-- Leave Team Confirmation Modal -->
<ConfirmationModal :show="confirmingLeavingTeam" @close="confirmingLeavingTeam = false">
<template #title>
Leave Team
</template>
<template #content>
Are you sure you would like to leave this team?
</template>
<template #footer>
<SecondaryButton @click="confirmingLeavingTeam = false">
Cancel
</SecondaryButton>
<DangerButton
class="ml-3"
:class="{ 'opacity-25': leaveTeamForm.processing }"
:disabled="leaveTeamForm.processing"
@click="leaveTeam"
>
Leave
</DangerButton>
</template>
</ConfirmationModal>
<!-- Remove Team Member Confirmation Modal -->
<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?
</template>
<template #footer>
<SecondaryButton @click="teamMemberBeingRemoved = null">
Cancel
</SecondaryButton>
<DangerButton
class="ml-3"
:class="{ 'opacity-25': removeTeamMemberForm.processing }"
:disabled="removeTeamMemberForm.processing"
@click="removeTeamMember"
>
Remove
</DangerButton>
</template>
</ConfirmationModal>
</div>
</template>

View File

@ -0,0 +1,80 @@
<script setup>
import { useForm } from '@inertiajs/inertia-vue3';
import ActionMessage from '@/Components/ActionMessage.vue';
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';
const props = defineProps({
team: Object,
permissions: Object,
});
const form = useForm({
name: props.team.name,
});
const updateTeamName = () => {
form.put(route('teams.update', props.team), {
errorBag: 'updateTeamName',
preserveScroll: true,
});
};
</script>
<template>
<FormSection @submitted="updateTeamName">
<template #title>
Team Name
</template>
<template #description>
The team's name and owner information.
</template>
<template #form>
<!-- Team Owner Information -->
<div class="col-span-6">
<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">
<div class="ml-4 leading-tight">
<div>{{ team.owner.name }}</div>
<div class="text-gray-700 text-sm">
{{ team.owner.email }}
</div>
</div>
</div>
</div>
<!-- Team Name -->
<div class="col-span-6 sm:col-span-4">
<InputLabel for="name" value="Team Name" />
<TextInput
id="name"
v-model="form.name"
type="text"
class="mt-1 block w-full"
:disabled="! permissions.canUpdateTeam"
/>
<InputError :message="form.errors.name" class="mt-2" />
</div>
</template>
<template v-if="permissions.canUpdateTeam" #actions>
<ActionMessage :on="form.recentlySuccessful" class="mr-3">
Saved.
</ActionMessage>
<PrimaryButton :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Save
</PrimaryButton>
</template>
</FormSection>
</template>

View File

@ -0,0 +1,42 @@
<script setup>
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';
defineProps({
team: Object,
availableRoles: Array,
permissions: Object,
});
</script>
<template>
<AppLayout title="Team Settings">
<template #header>
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
Team Settings
</h2>
</template>
<div>
<div class="max-w-7xl mx-auto py-10 sm:px-6 lg:px-8">
<UpdateTeamNameForm :team="team" :permissions="permissions" />
<TeamMemberManager
class="mt-10 sm:mt-0"
:team="team"
:available-roles="availableRoles"
:user-permissions="permissions"
/>
<template v-if="permissions.canDeleteTeam && ! team.personal_team">
<SectionBorder />
<DeleteTeamForm class="mt-10 sm:mt-0" :team="team" />
</template>
</div>
</div>
</AppLayout>
</template>

View File

@ -0,0 +1,24 @@
<script setup>
import { Head } from '@inertiajs/inertia-vue3';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
defineProps({
terms: String,
});
</script>
<template>
<Head title="Terms of Service" />
<div class="font-sans text-gray-900 antialiased">
<div class="pt-4 bg-gray-100">
<div class="min-h-screen flex flex-col items-center pt-6 sm:pt-0">
<div>
<AuthenticationCardLogo />
</div>
<div class="w-full sm:max-w-2xl mt-6 p-6 bg-white shadow-md overflow-hidden sm:rounded-lg prose" v-html="terms" />
</div>
</div>
</div>
</template>

View File

@ -0,0 +1,179 @@
<script setup>
import { Head, Link } from '@inertiajs/inertia-vue3';
defineProps({
canLogin: Boolean,
canRegister: Boolean,
laravelVersion: String,
phpVersion: String,
});
</script>
<template>
<Head title="Welcome" />
<div class="relative flex items-top justify-center min-h-screen bg-gray-100 dark:bg-gray-900 sm:items-center sm:pt-0">
<div v-if="canLogin" class="hidden fixed top-0 right-0 px-6 py-4 sm:block">
<Link v-if="$page.props.user" :href="route('dashboard')" class="text-sm text-gray-700 dark:text-gray-500 underline">Dashboard</Link>
<template v-else>
<Link :href="route('login')" class="text-sm text-gray-700 dark:text-gray-500 underline">Log in</Link>
<Link v-if="canRegister" :href="route('register')" class="ml-4 text-sm text-gray-700 dark:text-gray-500 underline">Register</Link>
</template>
</div>
<div class="max-w-6xl mx-auto sm:px-6 lg:px-8">
<div class="flex justify-center pt-8 sm:justify-start sm:pt-0">
<svg
viewBox="0 0 651 192"
fill="none"
xmlns="http://www.w3.org/2000/svg"
class="h-16 w-auto text-gray-700 sm:h-20"
>
<g clip-path="url(#clip0)" fill="#EF3B2D">
<path d="M248.032 44.676h-16.466v100.23h47.394v-14.748h-30.928V44.676zM337.091 87.202c-2.101-3.341-5.083-5.965-8.949-7.875-3.865-1.909-7.756-2.864-11.669-2.864-5.062 0-9.69.931-13.89 2.792-4.201 1.861-7.804 4.417-10.811 7.661-3.007 3.246-5.347 6.993-7.016 11.239-1.672 4.249-2.506 8.713-2.506 13.389 0 4.774.834 9.26 2.506 13.459 1.669 4.202 4.009 7.925 7.016 11.169 3.007 3.246 6.609 5.799 10.811 7.66 4.199 1.861 8.828 2.792 13.89 2.792 3.913 0 7.804-.955 11.669-2.863 3.866-1.908 6.849-4.533 8.949-7.875v9.021h15.607V78.182h-15.607v9.02zm-1.431 32.503c-.955 2.578-2.291 4.821-4.009 6.73-1.719 1.91-3.795 3.437-6.229 4.582-2.435 1.146-5.133 1.718-8.091 1.718-2.96 0-5.633-.572-8.019-1.718-2.387-1.146-4.438-2.672-6.156-4.582-1.719-1.909-3.032-4.152-3.938-6.73-.909-2.577-1.36-5.298-1.36-8.161 0-2.864.451-5.585 1.36-8.162.905-2.577 2.219-4.819 3.938-6.729 1.718-1.908 3.77-3.437 6.156-4.582 2.386-1.146 5.059-1.718 8.019-1.718 2.958 0 5.656.572 8.091 1.718 2.434 1.146 4.51 2.674 6.229 4.582 1.718 1.91 3.054 4.152 4.009 6.729.953 2.577 1.432 5.298 1.432 8.162-.001 2.863-.479 5.584-1.432 8.161zM463.954 87.202c-2.101-3.341-5.083-5.965-8.949-7.875-3.865-1.909-7.756-2.864-11.669-2.864-5.062 0-9.69.931-13.89 2.792-4.201 1.861-7.804 4.417-10.811 7.661-3.007 3.246-5.347 6.993-7.016 11.239-1.672 4.249-2.506 8.713-2.506 13.389 0 4.774.834 9.26 2.506 13.459 1.669 4.202 4.009 7.925 7.016 11.169 3.007 3.246 6.609 5.799 10.811 7.66 4.199 1.861 8.828 2.792 13.89 2.792 3.913 0 7.804-.955 11.669-2.863 3.866-1.908 6.849-4.533 8.949-7.875v9.021h15.607V78.182h-15.607v9.02zm-1.432 32.503c-.955 2.578-2.291 4.821-4.009 6.73-1.719 1.91-3.795 3.437-6.229 4.582-2.435 1.146-5.133 1.718-8.091 1.718-2.96 0-5.633-.572-8.019-1.718-2.387-1.146-4.438-2.672-6.156-4.582-1.719-1.909-3.032-4.152-3.938-6.73-.909-2.577-1.36-5.298-1.36-8.161 0-2.864.451-5.585 1.36-8.162.905-2.577 2.219-4.819 3.938-6.729 1.718-1.908 3.77-3.437 6.156-4.582 2.386-1.146 5.059-1.718 8.019-1.718 2.958 0 5.656.572 8.091 1.718 2.434 1.146 4.51 2.674 6.229 4.582 1.718 1.91 3.054 4.152 4.009 6.729.953 2.577 1.432 5.298 1.432 8.162 0 2.863-.479 5.584-1.432 8.161zM650.772 44.676h-15.606v100.23h15.606V44.676zM365.013 144.906h15.607V93.538h26.776V78.182h-42.383v66.724zM542.133 78.182l-19.616 51.096-19.616-51.096h-15.808l25.617 66.724h19.614l25.617-66.724h-15.808zM591.98 76.466c-19.112 0-34.239 15.706-34.239 35.079 0 21.416 14.641 35.079 36.239 35.079 12.088 0 19.806-4.622 29.234-14.688l-10.544-8.158c-.006.008-7.958 10.449-19.832 10.449-13.802 0-19.612-11.127-19.612-16.884h51.777c2.72-22.043-11.772-40.877-33.023-40.877zm-18.713 29.28c.12-1.284 1.917-16.884 18.589-16.884 16.671 0 18.697 15.598 18.813 16.884h-37.402zM184.068 43.892c-.024-.088-.073-.165-.104-.25-.058-.157-.108-.316-.191-.46-.056-.097-.137-.176-.203-.265-.087-.117-.161-.242-.265-.345-.085-.086-.194-.148-.29-.223-.109-.085-.206-.182-.327-.252l-.002-.001-.002-.002-35.648-20.524a2.971 2.971 0 00-2.964 0l-35.647 20.522-.002.002-.002.001c-.121.07-.219.167-.327.252-.096.075-.205.138-.29.223-.103.103-.178.228-.265.345-.066.089-.147.169-.203.265-.083.144-.133.304-.191.46-.031.085-.08.162-.104.25-.067.249-.103.51-.103.776v38.979l-29.706 17.103V24.493a3 3 0 00-.103-.776c-.024-.088-.073-.165-.104-.25-.058-.157-.108-.316-.191-.46-.056-.097-.137-.176-.203-.265-.087-.117-.161-.242-.265-.345-.085-.086-.194-.148-.29-.223-.109-.085-.206-.182-.327-.252l-.002-.001-.002-.002L40.098 1.396a2.971 2.971 0 00-2.964 0L1.487 21.919l-.002.002-.002.001c-.121.07-.219.167-.327.252-.096.075-.205.138-.29.223-.103.103-.178.228-.265.345-.066.089-.147.169-.203.265-.083.144-.133.304-.191.46-.031.085-.08.162-.104.25-.067.249-.103.51-.103.776v122.09c0 1.063.568 2.044 1.489 2.575l71.293 41.045c.156.089.324.143.49.202.078.028.15.074.23.095a2.98 2.98 0 001.524 0c.069-.018.132-.059.2-.083.176-.061.354-.119.519-.214l71.293-41.045a2.971 2.971 0 001.489-2.575v-38.979l34.158-19.666a2.971 2.971 0 001.489-2.575V44.666a3.075 3.075 0 00-.106-.774zM74.255 143.167l-29.648-16.779 31.136-17.926.001-.001 34.164-19.669 29.674 17.084-21.772 12.428-43.555 24.863zm68.329-76.259v33.841l-12.475-7.182-17.231-9.92V49.806l12.475 7.182 17.231 9.92zm2.97-39.335l29.693 17.095-29.693 17.095-29.693-17.095 29.693-17.095zM54.06 114.089l-12.475 7.182V46.733l17.231-9.92 12.475-7.182v74.537l-17.231 9.921zM38.614 7.398l29.693 17.095-29.693 17.095L8.921 24.493 38.614 7.398zM5.938 29.632l12.475 7.182 17.231 9.92v79.676l.001.005-.001.006c0 .114.032.221.045.333.017.146.021.294.059.434l.002.007c.032.117.094.222.14.334.051.124.088.255.156.371a.036.036 0 00.004.009c.061.105.149.191.222.288.081.105.149.22.244.314l.008.01c.084.083.19.142.284.215.106.083.202.178.32.247l.013.005.011.008 34.139 19.321v34.175L5.939 144.867V29.632h-.001zm136.646 115.235l-65.352 37.625V148.31l48.399-27.628 16.953-9.677v33.862zm35.646-61.22l-29.706 17.102V66.908l17.231-9.92 12.475-7.182v33.841z" />
</g>
</svg>
</div>
<div class="mt-8 bg-white dark:bg-gray-800 overflow-hidden shadow sm:rounded-lg">
<div class="grid grid-cols-1 md:grid-cols-2">
<div class="p-6">
<div class="flex items-center">
<svg
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
class="w-8 h-8 text-gray-500"
><path d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /></svg>
<div class="ml-4 text-lg leading-7 font-semibold">
<a href="https://laravel.com/docs" class="underline text-gray-900 dark:text-white">Documentation</a>
</div>
</div>
<div class="ml-12">
<div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
Laravel has wonderful, thorough documentation covering every aspect of the framework. Whether you are new to the framework or have previous experience with Laravel, we recommend reading all of the documentation from beginning to end.
</div>
</div>
</div>
<div class="p-6 border-t border-gray-200 dark:border-gray-700 md:border-t-0 md:border-l">
<div class="flex items-center">
<svg
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
class="w-8 h-8 text-gray-500"
><path d="M3 9a2 2 0 012-2h.93a2 2 0 001.664-.89l.812-1.22A2 2 0 0110.07 4h3.86a2 2 0 011.664.89l.812 1.22A2 2 0 0018.07 7H19a2 2 0 012 2v9a2 2 0 01-2 2H5a2 2 0 01-2-2V9z" /><path d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" /></svg>
<div class="ml-4 text-lg leading-7 font-semibold">
<a href="https://laracasts.com" class="underline text-gray-900 dark:text-white">Laracasts</a>
</div>
</div>
<div class="ml-12">
<div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
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.
</div>
</div>
</div>
<div class="p-6 border-t border-gray-200 dark:border-gray-700">
<div class="flex items-center">
<svg
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
class="w-8 h-8 text-gray-500"
><path d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" /></svg>
<div class="ml-4 text-lg leading-7 font-semibold">
<a href="https://laravel-news.com/" class="underline text-gray-900 dark:text-white">Laravel News</a>
</div>
</div>
<div class="ml-12">
<div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
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.
</div>
</div>
</div>
<div class="p-6 border-t border-gray-200 dark:border-gray-700 md:border-l">
<div class="flex items-center">
<svg
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
class="w-8 h-8 text-gray-500"
><path d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
<div class="ml-4 text-lg leading-7 font-semibold text-gray-900 dark:text-white">
Vibrant Ecosystem
</div>
</div>
<div class="ml-12">
<div class="mt-2 text-gray-600 dark:text-gray-400 text-sm">
Laravel's robust library of first-party tools and libraries, such as <a href="https://forge.laravel.com" class="underline">Forge</a>, <a href="https://vapor.laravel.com" class="underline">Vapor</a>, <a href="https://nova.laravel.com" class="underline">Nova</a>, and <a href="https://envoyer.io" class="underline">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">Cashier</a>, <a href="https://laravel.com/docs/dusk" class="underline">Dusk</a>, <a href="https://laravel.com/docs/broadcasting" class="underline">Echo</a>, <a href="https://laravel.com/docs/horizon" class="underline">Horizon</a>, <a href="https://laravel.com/docs/sanctum" class="underline">Sanctum</a>, <a href="https://laravel.com/docs/telescope" class="underline">Telescope</a>, and more.
</div>
</div>
</div>
</div>
</div>
<div class="flex justify-center mt-4 sm:items-center sm:justify-between">
<div class="text-center text-sm text-gray-500 sm:text-left">
<div class="flex items-center">
<svg
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
stroke="currentColor"
class="-mt-px w-5 h-5 text-gray-400"
>
<path d="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<a href="https://laravel.bigcartel.com" class="ml-1 underline">
Shop
</a>
<svg
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
viewBox="0 0 24 24"
class="ml-4 -mt-px w-5 h-5 text-gray-400"
>
<path d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z" />
</svg>
<a href="https://github.com/sponsors/taylorotwell" class="ml-1 underline">
Sponsor
</a>
</div>
</div>
<div class="ml-4 text-center text-sm text-gray-500 sm:text-right sm:ml-0">
Laravel v{{ laravelVersion }} (PHP v{{ phpVersion }})
</div>
</div>
</div>
</div>
</template>

23
resources/js/app.js Normal file
View File

@ -0,0 +1,23 @@
import './bootstrap';
import '../css/app.css';
import { createApp, h } from 'vue';
import { createInertiaApp } from '@inertiajs/inertia-vue3';
import { InertiaProgress } from '@inertiajs/progress';
import { resolvePageComponent } from 'laravel-vite-plugin/inertia-helpers';
import { ZiggyVue } from '../../vendor/tightenco/ziggy/dist/vue.m';
const appName = window.document.getElementsByTagName('title')[0]?.innerText || 'Laravel';
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) => resolvePageComponent(`./Pages/${name}.vue`, import.meta.glob('./Pages/**/*.vue')),
setup({ el, app, props, plugin }) {
return createApp({ render: () => h(app, props) })
.use(plugin)
.use(ZiggyVue, Ziggy)
.mount(el);
},
});
InertiaProgress.init({ color: '#4B5563' });

34
resources/js/bootstrap.js vendored Normal file
View File

@ -0,0 +1,34 @@
import _ from 'lodash';
window._ = _;
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo';
// import Pusher from 'pusher-js';
// window.Pusher = Pusher;
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: import.meta.env.VITE_PUSHER_APP_KEY,
// wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
// enabledTransports: ['ws', 'wss'],
// });

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

@ -0,0 +1,7 @@
const Ziggy = {"url":"http:\/\/localhost","port":null,"defaults":{},"routes":{"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"]},"password.email":{"uri":"forgot-password","methods":["POST"]},"password.update":{"uri":"reset-password","methods":["POST"]},"register":{"uri":"register","methods":["GET","HEAD"]},"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"]},"teams.update":{"uri":"teams\/{team}","methods":["PUT"]},"teams.destroy":{"uri":"teams\/{team}","methods":["DELETE"]},"current-team.update":{"uri":"current-team","methods":["PUT"]},"team-members.store":{"uri":"teams\/{team}\/members","methods":["POST"]},"team-members.update":{"uri":"teams\/{team}\/members\/{user}","methods":["PUT"]},"team-members.destroy":{"uri":"teams\/{team}\/members\/{user}","methods":["DELETE"]},"team-invitations.accept":{"uri":"team-invitations\/{invitation}","methods":["GET","HEAD"],"bindings":{"invitation":"id"}},"team-invitations.destroy":{"uri":"team-invitations\/{invitation}","methods":["DELETE"],"bindings":{"invitation":"id"}},"sanctum.csrf-cookie":{"uri":"sanctum\/csrf-cookie","methods":["GET","HEAD"]},"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 };