initial commit
This commit is contained in:
15
resources/js/Jetstream/ActionMessage.vue
Normal file
15
resources/js/Jetstream/ActionMessage.vue
Normal 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>
|
22
resources/js/Jetstream/ActionSection.vue
Normal file
22
resources/js/Jetstream/ActionSection.vue
Normal 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>
|
7
resources/js/Jetstream/ApplicationLogo.vue
Normal file
7
resources/js/Jetstream/ApplicationLogo.vue
Normal 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>
|
6
resources/js/Jetstream/ApplicationMark.vue
Normal file
6
resources/js/Jetstream/ApplicationMark.vue
Normal 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>
|
11
resources/js/Jetstream/AuthenticationCard.vue
Normal file
11
resources/js/Jetstream/AuthenticationCard.vue
Normal 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>
|
17
resources/js/Jetstream/AuthenticationCardLogo.vue
Normal file
17
resources/js/Jetstream/AuthenticationCardLogo.vue
Normal 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>
|
87
resources/js/Jetstream/Banner.vue
Normal file
87
resources/js/Jetstream/Banner.vue
Normal 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>
|
36
resources/js/Jetstream/Checkbox.vue
Normal file
36
resources/js/Jetstream/Checkbox.vue
Normal 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>
|
67
resources/js/Jetstream/ConfirmationModal.vue
Normal file
67
resources/js/Jetstream/ConfirmationModal.vue
Normal 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>
|
117
resources/js/Jetstream/ConfirmsPassword.vue
Normal file
117
resources/js/Jetstream/ConfirmsPassword.vue
Normal 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>
|
14
resources/js/Jetstream/DangerButton.vue
Normal file
14
resources/js/Jetstream/DangerButton.vue
Normal 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>
|
47
resources/js/Jetstream/DialogModal.vue
Normal file
47
resources/js/Jetstream/DialogModal.vue
Normal 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>
|
79
resources/js/Jetstream/Dropdown.vue
Normal file
79
resources/js/Jetstream/Dropdown.vue
Normal 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>
|
24
resources/js/Jetstream/DropdownLink.vue
Normal file
24
resources/js/Jetstream/DropdownLink.vue
Normal 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>
|
38
resources/js/Jetstream/FormSection.vue
Normal file
38
resources/js/Jetstream/FormSection.vue
Normal 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>
|
13
resources/js/Jetstream/InputError.vue
Normal file
13
resources/js/Jetstream/InputError.vue
Normal 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>
|
12
resources/js/Jetstream/InputLabel.vue
Normal file
12
resources/js/Jetstream/InputLabel.vue
Normal 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>
|
91
resources/js/Jetstream/Modal.vue
Normal file
91
resources/js/Jetstream/Modal.vue
Normal 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>
|
21
resources/js/Jetstream/NavLink.vue
Normal file
21
resources/js/Jetstream/NavLink.vue
Normal 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>
|
14
resources/js/Jetstream/PrimaryButton.vue
Normal file
14
resources/js/Jetstream/PrimaryButton.vue
Normal 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>
|
28
resources/js/Jetstream/ResponsiveNavLink.vue
Normal file
28
resources/js/Jetstream/ResponsiveNavLink.vue
Normal 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>
|
14
resources/js/Jetstream/SecondaryButton.vue
Normal file
14
resources/js/Jetstream/SecondaryButton.vue
Normal 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>
|
7
resources/js/Jetstream/SectionBorder.vue
Normal file
7
resources/js/Jetstream/SectionBorder.vue
Normal 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>
|
17
resources/js/Jetstream/SectionTitle.vue
Normal file
17
resources/js/Jetstream/SectionTitle.vue
Normal 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>
|
28
resources/js/Jetstream/TextInput.vue
Normal file
28
resources/js/Jetstream/TextInput.vue
Normal 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>
|
138
resources/js/Jetstream/Welcome.vue
Normal file
138
resources/js/Jetstream/Welcome.vue
Normal 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>
|
311
resources/js/Layouts/AppLayout.vue
Normal file
311
resources/js/Layouts/AppLayout.vue
Normal 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>
|
30
resources/js/Pages/API/Index.vue
Normal file
30
resources/js/Pages/API/Index.vue
Normal 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>
|
255
resources/js/Pages/API/Partials/ApiTokenManager.vue
Normal file
255
resources/js/Pages/API/Partials/ApiTokenManager.vue
Normal 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>
|
63
resources/js/Pages/Auth/ConfirmPassword.vue
Normal file
63
resources/js/Pages/Auth/ConfirmPassword.vue
Normal 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>
|
60
resources/js/Pages/Auth/ForgotPassword.vue
Normal file
60
resources/js/Pages/Auth/ForgotPassword.vue
Normal 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>
|
89
resources/js/Pages/Auth/Login.vue
Normal file
89
resources/js/Pages/Auth/Login.vue
Normal 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>
|
111
resources/js/Pages/Auth/Register.vue
Normal file
111
resources/js/Pages/Auth/Register.vue
Normal 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>
|
84
resources/js/Pages/Auth/ResetPassword.vue
Normal file
84
resources/js/Pages/Auth/ResetPassword.vue
Normal 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>
|
104
resources/js/Pages/Auth/TwoFactorChallenge.vue
Normal file
104
resources/js/Pages/Auth/TwoFactorChallenge.vue
Normal 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>
|
62
resources/js/Pages/Auth/VerifyEmail.vue
Normal file
62
resources/js/Pages/Auth/VerifyEmail.vue
Normal 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>
|
22
resources/js/Pages/Dashboard.vue
Normal file
22
resources/js/Pages/Dashboard.vue
Normal 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>
|
24
resources/js/Pages/PrivacyPolicy.vue
Normal file
24
resources/js/Pages/PrivacyPolicy.vue
Normal 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>
|
101
resources/js/Pages/Profile/Partials/DeleteUserForm.vue
Normal file
101
resources/js/Pages/Profile/Partials/DeleteUserForm.vue
Normal 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>
|
@ -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>
|
@ -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>
|
100
resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue
Normal file
100
resources/js/Pages/Profile/Partials/UpdatePasswordForm.vue
Normal 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>
|
@ -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>
|
57
resources/js/Pages/Profile/Show.vue
Normal file
57
resources/js/Pages/Profile/Show.vue
Normal 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>
|
20
resources/js/Pages/Teams/Create.vue
Normal file
20
resources/js/Pages/Teams/Create.vue
Normal 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>
|
66
resources/js/Pages/Teams/Partials/CreateTeamForm.vue
Normal file
66
resources/js/Pages/Teams/Partials/CreateTeamForm.vue
Normal 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>
|
75
resources/js/Pages/Teams/Partials/DeleteTeamForm.vue
Normal file
75
resources/js/Pages/Teams/Partials/DeleteTeamForm.vue
Normal 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>
|
396
resources/js/Pages/Teams/Partials/TeamMemberManager.vue
Normal file
396
resources/js/Pages/Teams/Partials/TeamMemberManager.vue
Normal 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>
|
80
resources/js/Pages/Teams/Partials/UpdateTeamNameForm.vue
Normal file
80
resources/js/Pages/Teams/Partials/UpdateTeamNameForm.vue
Normal 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>
|
42
resources/js/Pages/Teams/Show.vue
Normal file
42
resources/js/Pages/Teams/Show.vue
Normal 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>
|
24
resources/js/Pages/TermsOfService.vue
Normal file
24
resources/js/Pages/TermsOfService.vue
Normal 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>
|
179
resources/js/Pages/Welcome.vue
Normal file
179
resources/js/Pages/Welcome.vue
Normal 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
23
resources/js/app.js
Normal 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
34
resources/js/bootstrap.js
vendored
Normal 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
7
resources/js/ziggy.js
Normal 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 };
|
Reference in New Issue
Block a user