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

* add linter, prettier and typescript support for jetstream

* add github actions for lint, build and typecheck

* add composer install to build action

* fix composer lock

* add ext-intl and fixed php version

* fix intl php extension install

* add tip php extension to github npm build action

* fix php version to 8.3.1

* change github php base action

* fix primary button default type

* updated typescript types from Team to Organization

---------

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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