[FEAT] 完善個人檔案功能:新增頭像即時上傳、麵包屑導覽、版面寬度優化與日期格式統一
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 58s
All checks were successful
star-cloud-deploy-demo / deploy-demo (push) Successful in 58s
This commit is contained in:
@@ -1,58 +0,0 @@
|
||||
<section class="space-y-6">
|
||||
<header>
|
||||
<h2 class="text-xl font-black text-rose-600 dark:text-rose-500 tracking-tight">
|
||||
{{ __('Danger Zone: Delete Account') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-1 text-sm font-bold text-slate-400 dark:text-slate-400 uppercase tracking-widest">
|
||||
{{ __('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.') }}
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<button
|
||||
x-data=""
|
||||
x-on:click.prevent="$dispatch('open-modal', 'confirm-user-deletion')"
|
||||
class="btn-luxury-rose px-8"
|
||||
>
|
||||
<span>{{ __('Delete Account') }}</span>
|
||||
</button>
|
||||
|
||||
<x-modal name="confirm-user-deletion" :show="$errors->userDeletion->isNotEmpty()" focusable>
|
||||
<form method="post" action="{{ route('profile.destroy') }}" class="p-8">
|
||||
@csrf
|
||||
@method('delete')
|
||||
|
||||
<h2 class="text-2xl font-black text-slate-800 dark:text-white tracking-tight">
|
||||
{{ __('Are you sure you want to delete your account?') }}
|
||||
</h2>
|
||||
|
||||
<p class="mt-3 text-sm font-bold text-slate-500 dark:text-slate-400 leading-relaxed">
|
||||
{{ __('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.') }}
|
||||
</p>
|
||||
|
||||
<div class="mt-8">
|
||||
<x-input-label for="password" value="{{ __('Password') }}" class="sr-only" />
|
||||
|
||||
<input
|
||||
id="password"
|
||||
name="password"
|
||||
type="password"
|
||||
class="py-3 px-4 block w-full border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900 rounded-2xl text-sm font-bold text-slate-700 dark:text-slate-200 focus:ring-4 focus:ring-rose-500/10 focus:border-rose-500 transition-all outline-none"
|
||||
placeholder="{{ __('Enter your password to confirm') }}"
|
||||
/>
|
||||
|
||||
<x-input-error :messages="$errors->userDeletion->get('password')" class="mt-2" />
|
||||
</div>
|
||||
|
||||
<div class="mt-8 flex justify-end gap-x-3">
|
||||
<button type="button" x-on:click="$dispatch('close')" class="py-3 px-6 inline-flex items-center gap-x-2 text-sm font-black rounded-2xl border border-slate-200 bg-white text-slate-600 shadow-sm hover:bg-slate-50 focus:outline-none focus:bg-slate-50 disabled:opacity-50 disabled:pointer-events-none dark:bg-slate-900 dark:border-slate-700 dark:text-slate-400 dark:hover:bg-slate-800 dark:focus:bg-slate-800 transition-all">
|
||||
{{ __('Cancel') }}
|
||||
</button>
|
||||
|
||||
<button type="submit" class="btn-luxury-rose px-8">
|
||||
<span>{{ __('Permanently Delete Account') }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</x-modal>
|
||||
</section>
|
||||
@@ -1,46 +1,62 @@
|
||||
<div class="space-y-6">
|
||||
<div class="flex items-center gap-x-3 mb-6">
|
||||
<div class="size-10 rounded-xl bg-cyan-500/10 flex items-center justify-center text-cyan-600 dark:text-cyan-400">
|
||||
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
<div class="flex items-center justify-between mb-8">
|
||||
<div class="flex items-center gap-x-4">
|
||||
<div class="size-12 rounded-2xl bg-cyan-500/10 flex items-center justify-center text-cyan-600 dark:text-cyan-400 border border-cyan-500/20 shadow-lg shadow-cyan-500/5">
|
||||
<svg class="size-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="text-xl font-black text-slate-800 dark:text-white tracking-tight">{{ __('Login History') }}</h3>
|
||||
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-[0.2em]">{{ __('Your recent account activity') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="text-lg font-black text-slate-800 dark:text-white tracking-tight">{{ __('Login History') }}</h3>
|
||||
</div>
|
||||
|
||||
<div class="flow-root">
|
||||
<ul role="list" class="-mb-8">
|
||||
@forelse($user->loginLogs as $log)
|
||||
<li>
|
||||
<div class="relative pb-8">
|
||||
@if(!$loop->last)
|
||||
<span class="absolute left-4 top-4 -ml-px h-full w-0.5 bg-slate-100 dark:bg-slate-800" aria-hidden="true"></span>
|
||||
@endif
|
||||
<div class="relative flex space-x-3 mt-1">
|
||||
<div>
|
||||
<span class="h-8 w-8 rounded-full bg-slate-100 dark:bg-slate-800 flex items-center justify-center ring-8 ring-white dark:ring-slate-900">
|
||||
<svg class="h-4 w-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"/>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="relative">
|
||||
<!-- Vertical Line -->
|
||||
<div class="absolute left-6 top-2 bottom-2 w-px bg-gradient-to-b from-slate-200 via-slate-200 to-transparent dark:from-slate-800 dark:via-slate-800 dark:to-transparent"></div>
|
||||
|
||||
<div class="space-y-6">
|
||||
@forelse($user->loginLogs()->latest()->take(10)->get() as $log)
|
||||
<div class="relative pl-14 group">
|
||||
<!-- Dot -->
|
||||
<div class="absolute left-[21px] top-3 size-2.5 rounded-full border-2 border-white dark:border-slate-900 bg-slate-300 dark:bg-slate-700 group-hover:bg-cyan-500 group-hover:scale-125 transition-all duration-300 z-10"></div>
|
||||
|
||||
<div class="luxury-card p-5 rounded-2xl border border-slate-100 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/50 hover:bg-white dark:hover:bg-slate-800/80 transition-all duration-300">
|
||||
<div class="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-x-3 mb-2">
|
||||
<span class="text-sm font-black text-slate-700 dark:text-slate-200 font-mono tracking-tight">{{ $log->ip_address }}</span>
|
||||
<span class="size-1 rounded-full bg-slate-300 dark:bg-slate-600"></span>
|
||||
<span class="text-[10px] font-black text-cyan-500 dark:text-cyan-400 uppercase tracking-widest bg-cyan-500/5 px-2 py-0.5 rounded-md border border-cyan-500/10">
|
||||
{{ __('Success') }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="text-xs font-medium text-slate-400 dark:text-slate-500 break-all leading-relaxed" title="{{ $log->user_agent }}">
|
||||
<span class="font-bold text-slate-500 dark:text-slate-400 mr-1 italic">{{ __('Device:') }}</span>
|
||||
{{ $log->user_agent }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex min-w-0 flex-1 justify-between space-x-4 pt-1.5">
|
||||
<div>
|
||||
<p class="text-[11px] font-black text-slate-700 dark:text-slate-300 uppercase tracking-widest">{{ $log->ip_address }}</p>
|
||||
<p class="mt-1 text-xs text-slate-400 truncate max-w-[120px]" title="{{ $log->user_agent }}">
|
||||
{{ Str::limit($log->user_agent, 20) }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="whitespace-nowrap text-right text-[10px] font-bold text-slate-400">
|
||||
<time datetime="{{ $log->login_at }}">{{ $log->login_at->diffForHumans() }}</time>
|
||||
</div>
|
||||
<div class="shrink-0 text-right">
|
||||
<p class="text-[10px] font-black text-slate-500 dark:text-slate-400 uppercase tracking-widest mb-1">
|
||||
{{ $log->login_at->format('Y/m/d') }}
|
||||
</p>
|
||||
<p class="text-xs font-bold text-slate-400 dark:text-slate-500">
|
||||
{{ $log->login_at->diffForHumans() }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
@empty
|
||||
<li class="py-4 text-center text-xs text-slate-400">{{ __('No login history yet') }}</li>
|
||||
<div class="luxury-card p-12 text-center rounded-[2rem] border-dashed border-2 border-slate-100 dark:border-slate-800">
|
||||
<div class="size-16 rounded-full bg-slate-50 dark:bg-slate-900 flex items-center justify-center mx-auto mb-4">
|
||||
<svg class="size-8 text-slate-300 dark:text-slate-700" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
|
||||
</div>
|
||||
<p class="text-sm font-bold text-slate-400">{{ __('No login history yet') }}</p>
|
||||
</div>
|
||||
@endforelse
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -13,53 +13,13 @@
|
||||
@csrf
|
||||
</form>
|
||||
|
||||
<form method="post" action="{{ route('profile.update') }}" class="mt-8 space-y-6" enctype="multipart/form-data">
|
||||
<form id="profile-update-form" method="post" action="{{ route('profile.update') }}" class="mt-8 space-y-6" enctype="multipart/form-data">
|
||||
@csrf
|
||||
@method('patch')
|
||||
|
||||
<!-- Avatar Upload -->
|
||||
<div x-data="{ photoName: null, photoPreview: null }" class="flex flex-col items-center gap-y-4 mb-8 bg-slate-50/50 dark:bg-slate-900/50 p-6 rounded-3xl border border-slate-100 dark:border-slate-800">
|
||||
<div class="relative group">
|
||||
<!-- Current Avatar / Preview -->
|
||||
<div class="size-24 rounded-full overflow-hidden ring-4 ring-white dark:ring-gray-800 shadow-xl">
|
||||
<template x-if="! photoPreview">
|
||||
<img src="{{ $user->avatar_url }}" class="size-full object-cover" alt="{{ $user->name }}">
|
||||
</template>
|
||||
<template x-if="photoPreview">
|
||||
<img :src="photoPreview" class="size-full object-cover">
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- Upload Overlay -->
|
||||
<label for="avatar" class="absolute inset-0 flex items-center justify-center bg-black/40 text-white rounded-full opacity-0 group-hover:opacity-100 transition-opacity cursor-pointer">
|
||||
<svg class="size-6" 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="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 stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 13a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="text-center">
|
||||
<input type="file" id="avatar" name="avatar" class="hidden"
|
||||
accept="image/*"
|
||||
@change="
|
||||
photoName = $event.target.files[0].name;
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
photoPreview = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL($event.target.files[0]);
|
||||
">
|
||||
<button type="button" @click="$refs.avatarInput.click()" class="text-xs font-black text-cyan-600 dark:text-cyan-500 uppercase tracking-widest hover:text-cyan-700 transition-colors">
|
||||
{{ __('Change Avatar') }}
|
||||
</button>
|
||||
<p class="mt-1 text-[10px] text-slate-400 font-bold uppercase tracking-wider">{{ __('JPG, PNG or GIF. Max 2MB.') }}</p>
|
||||
<x-input-error class="mt-2" :messages="$errors->get('avatar')" />
|
||||
</div>
|
||||
|
||||
<!-- Hidden ref for clicking -->
|
||||
<input type="file" x-ref="avatarInput" class="hidden" accept="image/*" @change="/* same as above but easier to click */">
|
||||
</div>
|
||||
<!-- Hidden Avatar Input -->
|
||||
<input type="file" name="avatar" class="hidden" accept="image/*"
|
||||
@change="uploadAvatar($event)">
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user