versão 00
This commit is contained in:
parent
cb4a5ab5ab
commit
d5b3b23e49
15
package-lock.json
generated
15
package-lock.json
generated
@ -15,7 +15,8 @@
|
|||||||
"pinia": "^2.1.3",
|
"pinia": "^2.1.3",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.2.2",
|
"vue-router": "^4.2.2",
|
||||||
"vuetify": "^3.4.0",
|
"vue-the-mask": "^0.11.1",
|
||||||
|
"vuetify": "^3.7.11",
|
||||||
"vuex": "^4.1.0"
|
"vuex": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@ -11059,10 +11060,16 @@
|
|||||||
"vue": "^3.2.0"
|
"vue": "^3.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/vue-the-mask": {
|
||||||
|
"version": "0.11.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/vue-the-mask/-/vue-the-mask-0.11.1.tgz",
|
||||||
|
"integrity": "sha512-UquSfnSWejD0zAfcD+3jJ1chUAkOAyoxya9Lxh9acCRtrlmGcAIvd0cQYraWqKenbuZJUdum+S174atv2AuEHQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/vuetify": {
|
"node_modules/vuetify": {
|
||||||
"version": "3.7.8",
|
"version": "3.7.11",
|
||||||
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.8.tgz",
|
"resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.11.tgz",
|
||||||
"integrity": "sha512-67p7Ton7EikHKhzauAmpsV1HT/zA2DmG8hjcHmQrktGl1eBrRaQVE+idWLtbhO51jqpTsrKQK9+HuG0KRwIGYQ==",
|
"integrity": "sha512-50Z2SNwPXbkGmve4CwxOs4sySZGgLwQYIDsKx+coSrfIBqz8IyXgxRFQdrvgoehIwUjGTNqaPZymuK5rMFkfHA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^12.20 || >=14.13"
|
"node": "^12.20 || >=14.13"
|
||||||
|
|||||||
@ -21,7 +21,8 @@
|
|||||||
"pinia": "^2.1.3",
|
"pinia": "^2.1.3",
|
||||||
"vue": "^3.3.4",
|
"vue": "^3.3.4",
|
||||||
"vue-router": "^4.2.2",
|
"vue-router": "^4.2.2",
|
||||||
"vuetify": "^3.4.0",
|
"vue-the-mask": "^0.11.1",
|
||||||
|
"vuetify": "^3.7.11",
|
||||||
"vuex": "^4.1.0"
|
"vuex": "^4.1.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -1,218 +1,166 @@
|
|||||||
|
<!-- ProfilePage.vue -->
|
||||||
<template>
|
<template>
|
||||||
<div class="profile-container">
|
<div class="profile-container">
|
||||||
<div class="profile-header">
|
<div class="profile-header">
|
||||||
<h1 class="profile-title">{{ $t('profile.title') }}</h1>
|
<h1 class="profile-title">Perfil do Usuário</h1>
|
||||||
<p class="profile-subtitle">{{ $t('profile.subtitle') }}</p>
|
<p class="profile-subtitle">Gerencie suas informações pessoais</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form
|
<form @submit.prevent="saveProfile" class="profile-form">
|
||||||
@submit.prevent="submitProfile"
|
|
||||||
class="profile-form"
|
|
||||||
novalidate
|
|
||||||
aria-labelledby="profile-form-title"
|
|
||||||
>
|
|
||||||
<div class="profile-grid">
|
<div class="profile-grid">
|
||||||
<!-- Photo Section with Advanced Upload -->
|
<!-- Coluna da Foto -->
|
||||||
<div class="photo-section">
|
<div class="photo-section">
|
||||||
<div
|
<div class="profile-photo-container">
|
||||||
class="profile-photo-wrapper"
|
<div class="photo-wrapper">
|
||||||
:class="{ 'has-error': photoError }"
|
<img :src="profileData.photoUrl || defaultPhoto" alt="Foto de Perfil" class="profile-photo">
|
||||||
>
|
<div class="photo-overlay">
|
||||||
<img
|
<span class="photo-text">Alterar foto</span>
|
||||||
:src="previewPhotoUrl || defaultPhoto"
|
</div>
|
||||||
:alt="$t('profile.photoAlt')"
|
|
||||||
class="profile-photo"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
class="photo-upload-overlay"
|
|
||||||
@click="openFileDialog"
|
|
||||||
@keydown.enter="openFileDialog"
|
|
||||||
tabindex="0"
|
|
||||||
role="button"
|
|
||||||
:aria-label="$t('profile.changePhoto')"
|
|
||||||
>
|
|
||||||
<i class="fas fa-camera"></i>
|
|
||||||
<span>{{ $t('profile.changePhoto') }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
|
@change="handlePhotoUpload"
|
||||||
|
accept="image/*"
|
||||||
ref="photoInput"
|
ref="photoInput"
|
||||||
accept="image/jpeg,image/png,image/webp"
|
class="photo-input"
|
||||||
@change="handlePhotoUpload"
|
|
||||||
class="hidden-file-input"
|
|
||||||
>
|
>
|
||||||
|
<button type="button" @click="triggerPhotoUpload" class="upload-btn">
|
||||||
|
<i class="fas fa-camera"></i>
|
||||||
|
Alterar Foto
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<transition name="fade">
|
|
||||||
<div v-if="photoError" class="error-text">
|
|
||||||
{{ photoError }}
|
|
||||||
</div>
|
|
||||||
</transition>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Profile Information Section -->
|
<!-- Coluna das Informações -->
|
||||||
<div class="profile-information">
|
<div class="info-section">
|
||||||
<!-- Basic Information -->
|
<!-- Seção de Informações Básicas -->
|
||||||
<div class="info-section">
|
<div class="section-title">
|
||||||
<h2 class="section-heading">
|
<i class="fas fa-user"></i>
|
||||||
<i class="fas fa-user-circle"></i>
|
<h2 class="sub-title">Informações Básicas</h2>
|
||||||
{{ $t('profile.basicInfo') }}
|
</div>
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-row">
|
||||||
<div class="form-field">
|
<div class="form-group">
|
||||||
<label for="firstName">{{ $t('profile.firstName') }}</label>
|
<label>ID</label>
|
||||||
<input
|
<input type="text" v-model="profileData.id" readonly class="form-input readonly">
|
||||||
id="firstName"
|
|
||||||
v-model.trim="profileData.firstName"
|
|
||||||
type="text"
|
|
||||||
:class="{ 'is-invalid': v$.firstName.$error }"
|
|
||||||
@blur="v$.firstName.$touch()"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="v$.firstName.$error"
|
|
||||||
class="validation-error"
|
|
||||||
>
|
|
||||||
{{ $t('validation.requiredField') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="lastName">{{ $t('profile.lastName') }}</label>
|
|
||||||
<input
|
|
||||||
id="lastName"
|
|
||||||
v-model.trim="profileData.lastName"
|
|
||||||
type="text"
|
|
||||||
:class="{ 'is-invalid': v$.lastName.$error }"
|
|
||||||
@blur="v$.lastName.$touch()"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="v$.lastName.$error"
|
|
||||||
class="validation-error"
|
|
||||||
>
|
|
||||||
{{ $t('validation.requiredField') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Contact Information -->
|
<div class="form-row">
|
||||||
<div class="info-section">
|
<div class="form-group">
|
||||||
<h2 class="section-heading">
|
<label>Nome</label>
|
||||||
<i class="fas fa-envelope"></i>
|
<input
|
||||||
{{ $t('profile.contactInfo') }}
|
type="text"
|
||||||
</h2>
|
v-model="profileData.firstName"
|
||||||
|
required
|
||||||
|
class="form-input"
|
||||||
|
placeholder="Digite seu nome"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-group">
|
||||||
<div class="form-field">
|
<label>Sobrenome</label>
|
||||||
<label for="email">{{ $t('profile.email') }}</label>
|
<input
|
||||||
<input
|
type="text"
|
||||||
id="email"
|
v-model="profileData.lastName"
|
||||||
v-model.trim="profileData.email"
|
required
|
||||||
type="email"
|
class="form-input"
|
||||||
:class="{ 'is-invalid': v$.email.$error }"
|
placeholder="Digite seu sobrenome"
|
||||||
@blur="v$.email.$touch()"
|
>
|
||||||
required
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="v$.email.$error"
|
|
||||||
class="validation-error"
|
|
||||||
>
|
|
||||||
{{ $t('validation.invalidEmail') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
|
||||||
<label for="phone">{{ $t('profile.phone') }}</label>
|
|
||||||
<input
|
|
||||||
id="phone"
|
|
||||||
v-model="profileData.phone"
|
|
||||||
type="tel"
|
|
||||||
v-mask="'(##) #####-####'"
|
|
||||||
placeholder="(00) 00000-0000"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Professional Information -->
|
<!-- Seção de Contato -->
|
||||||
<div class="info-section">
|
<div class="section-title">
|
||||||
<h2 class="section-heading">
|
<i class="fas fa-address-card"></i>
|
||||||
<i class="fas fa-briefcase"></i>
|
<h2>Informações de Contato</h2>
|
||||||
{{ $t('profile.professionalInfo') }}
|
</div>
|
||||||
</h2>
|
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-row">
|
||||||
<div class="form-field">
|
<div class="form-group">
|
||||||
<label for="role">{{ $t('profile.role') }}</label>
|
<label>Email</label>
|
||||||
<select
|
<input
|
||||||
id="role"
|
type="email"
|
||||||
v-model="profileData.role"
|
v-model="profileData.email"
|
||||||
:class="{ 'is-invalid': v$.role.$error }"
|
required
|
||||||
@blur="v$.role.$touch()"
|
class="form-input"
|
||||||
required
|
placeholder="seu@email.com"
|
||||||
>
|
>
|
||||||
<option value="" disabled>
|
</div>
|
||||||
{{ $t('profile.selectRole') }}
|
|
||||||
</option>
|
|
||||||
<option
|
|
||||||
v-for="role in roles"
|
|
||||||
:key="role.id"
|
|
||||||
:value="role.id"
|
|
||||||
>
|
|
||||||
{{ role.name }}
|
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
<div
|
|
||||||
v-if="v$.role.$error"
|
|
||||||
class="validation-error"
|
|
||||||
>
|
|
||||||
{{ $t('validation.requiredField') }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-field">
|
<div class="form-group">
|
||||||
<label for="group">{{ $t('profile.group') }}</label>
|
<label>Telefone</label>
|
||||||
<select
|
<input
|
||||||
id="group"
|
type="tel"
|
||||||
v-model="profileData.group"
|
v-model="profileData.phone"
|
||||||
>
|
v-mask="'(##) #####-####'"
|
||||||
<option value="" disabled>
|
class="form-input"
|
||||||
{{ $t('profile.selectGroup') }}
|
placeholder="(00) 00000-0000"
|
||||||
</option>
|
>
|
||||||
<option
|
</div>
|
||||||
v-for="group in groups"
|
</div>
|
||||||
:key="group.id"
|
|
||||||
:value="group.id"
|
<!-- Seção Profissional -->
|
||||||
>
|
<div class="section-title">
|
||||||
{{ group.name }}
|
<i class="fas fa-briefcase"></i>
|
||||||
</option>
|
<h2>Informações Profissionais</h2>
|
||||||
</select>
|
</div>
|
||||||
</div>
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Cargo</label>
|
||||||
|
<select v-model="profileData.role" class="form-select">
|
||||||
|
<option value="" disabled>Selecione um cargo</option>
|
||||||
|
<option v-for="role in roles" :key="role.id" :value="role.id">
|
||||||
|
{{ role.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Grupo</label>
|
||||||
|
<select v-model="profileData.group" class="form-select">
|
||||||
|
<option value="" disabled>Selecione um grupo</option>
|
||||||
|
<option v-for="group in groups" :key="group.id" :value="group.id">
|
||||||
|
{{ group.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-row">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Permissão</label>
|
||||||
|
<select v-model="profileData.permission" class="form-select">
|
||||||
|
<option value="" disabled>Selecione uma permissão</option>
|
||||||
|
<option v-for="permission in permissions" :key="permission.id" :value="permission.id">
|
||||||
|
{{ permission.name }}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Turno</label>
|
||||||
|
<select v-model="profileData.shift" class="form-select">
|
||||||
|
<option value="" disabled>Selecione um turno</option>
|
||||||
|
<option value="morning">Manhã</option>
|
||||||
|
<option value="afternoon">Tarde</option>
|
||||||
|
<option value="night">Noite</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Action Buttons -->
|
<!-- Botões de Ação -->
|
||||||
<div class="form-actions">
|
<div class="button-group">
|
||||||
<button
|
<button type="button" @click="resetForm" class="cancel-btn">
|
||||||
type="button"
|
|
||||||
class="btn btn-secondary"
|
|
||||||
@click="resetForm"
|
|
||||||
:disabled="isSubmitting"
|
|
||||||
>
|
|
||||||
<i class="fas fa-times"></i>
|
<i class="fas fa-times"></i>
|
||||||
{{ $t('profile.cancel') }}
|
Cancelar
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button type="submit" class="save-btn">
|
||||||
type="submit"
|
|
||||||
class="btn btn-primary"
|
|
||||||
:disabled="isSubmitting || !isFormValid"
|
|
||||||
>
|
|
||||||
<i class="fas fa-save"></i>
|
<i class="fas fa-save"></i>
|
||||||
{{ $t('profile.save') }}
|
Salvar Alterações
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
@ -220,28 +168,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { ref } from 'vue'
|
|
||||||
import { useVuelidate } from '@vuelidate/core'
|
|
||||||
import { required, email } from '@vuelidate/validators'
|
|
||||||
import useProfileService from '@/services/profileService'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ProfilePage',
|
name: 'ProfilePage',
|
||||||
setup() {
|
|
||||||
const profileService = useProfileService()
|
|
||||||
const v$ = useVuelidate()
|
|
||||||
const previewPhotoUrl = ref(null)
|
|
||||||
const photoError = ref(null)
|
|
||||||
const isSubmitting = ref(false)
|
|
||||||
|
|
||||||
return {
|
|
||||||
v$,
|
|
||||||
previewPhotoUrl,
|
|
||||||
photoError,
|
|
||||||
isSubmitting,
|
|
||||||
profileService
|
|
||||||
}
|
|
||||||
},
|
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
defaultPhoto: '/path/to/default-avatar.png',
|
defaultPhoto: '/path/to/default-avatar.png',
|
||||||
@ -253,208 +181,321 @@ export default {
|
|||||||
email: '',
|
email: '',
|
||||||
phone: '',
|
phone: '',
|
||||||
role: '',
|
role: '',
|
||||||
group: ''
|
group: '',
|
||||||
|
permission: '',
|
||||||
|
shift: ''
|
||||||
},
|
},
|
||||||
roles: [
|
roles: [
|
||||||
{ id: 1, name: 'Employee' },
|
{ id: 1, name: 'Funcionário' },
|
||||||
{ id: 2, name: 'Intern' },
|
{ id: 2, name: 'Estagiário' },
|
||||||
{ id: 3, name: 'Manager' }
|
{ id: 3, name: 'Gerente' }
|
||||||
],
|
],
|
||||||
groups: [
|
groups: [
|
||||||
{ id: 1, name: 'Beta' },
|
{ id: 1, name: 'Beta' },
|
||||||
{ id: 2, name: 'Alpha' },
|
{ id: 2, name: 'Alfa' },
|
||||||
{ id: 3, name: 'Omega' }
|
{ id: 3, name: 'Omega' }
|
||||||
|
],
|
||||||
|
permissions: [
|
||||||
|
{ id: 1, name: 'Administrador' },
|
||||||
|
{ id: 2, name: 'Editor' },
|
||||||
|
{ id: 3, name: 'Visualizador' }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
isFormValid() {
|
|
||||||
return !this.v$.$error &&
|
|
||||||
this.profileData.firstName &&
|
|
||||||
this.profileData.lastName &&
|
|
||||||
this.profileData.email &&
|
|
||||||
this.profileData.role
|
|
||||||
}
|
|
||||||
},
|
|
||||||
validations() {
|
|
||||||
return {
|
|
||||||
profileData: {
|
|
||||||
firstName: { required },
|
|
||||||
lastName: { required },
|
|
||||||
email: { required, email },
|
|
||||||
role: { required }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
async loadProfile() {
|
async loadProfile() {
|
||||||
try {
|
try {
|
||||||
const profile = await this.profileService.getProfile()
|
const response = await fetch('/api/profile');
|
||||||
this.profileData = { ...profile }
|
const data = await response.json();
|
||||||
this.previewPhotoUrl = profile.photoUrl
|
this.profileData = { ...data };
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError(error)
|
console.error('Erro ao carregar perfil:', error);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
openFileDialog() {
|
triggerPhotoUpload() {
|
||||||
this.$refs.photoInput.click()
|
this.$refs.photoInput.click();
|
||||||
},
|
},
|
||||||
handlePhotoUpload(event) {
|
handlePhotoUpload(event) {
|
||||||
const file = event.target.files[0]
|
const file = event.target.files[0];
|
||||||
this.photoError = null
|
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
const maxSize = 5 * 1024 * 1024 // 5MB
|
const reader = new FileReader();
|
||||||
const validTypes = ['image/jpeg', 'image/png', 'image/webp']
|
|
||||||
|
|
||||||
if (file.size > maxSize) {
|
|
||||||
this.photoError = this.$t('validation.photoSizeTooLarge')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!validTypes.includes(file.type)) {
|
|
||||||
this.photoError = this.$t('validation.invalidPhotoType')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const reader = new FileReader()
|
|
||||||
reader.onload = (e) => {
|
reader.onload = (e) => {
|
||||||
this.previewPhotoUrl = e.target.result
|
this.profileData.photoUrl = e.target.result;
|
||||||
this.profileData.photoUrl = e.target.result
|
};
|
||||||
}
|
reader.readAsDataURL(file);
|
||||||
reader.readAsDataURL(file)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async submitProfile() {
|
async saveProfile() {
|
||||||
this.v$.$touch()
|
|
||||||
|
|
||||||
if (this.v$.$invalid) return
|
|
||||||
|
|
||||||
this.isSubmitting = true
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.profileService.updateProfile(this.profileData)
|
await fetch('/api/profile', {
|
||||||
this.$emit('profile-updated')
|
method: 'PUT',
|
||||||
this.$toast.success(this.$t('profile.updateSuccess'))
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(this.profileData)
|
||||||
|
});
|
||||||
|
|
||||||
|
this.$emit('profile-updated');
|
||||||
|
this.showNotification('Perfil atualizado com sucesso!', 'success');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.handleError(error)
|
console.error('Erro ao salvar perfil:', error);
|
||||||
} finally {
|
this.showNotification('Erro ao salvar perfil. Tente novamente.', 'error');
|
||||||
this.isSubmitting = false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
resetForm() {
|
resetForm() {
|
||||||
this.v$.$reset()
|
this.loadProfile();
|
||||||
this.loadProfile()
|
|
||||||
this.previewPhotoUrl = this.profileData.photoUrl
|
|
||||||
},
|
},
|
||||||
handleError(error) {
|
showNotification(message,type) {
|
||||||
console.error('Profile error:', error)
|
// Implementar sistema de notificação de sua preferência
|
||||||
this.$toast.error(
|
alert(message);
|
||||||
error.response?.data?.message ||
|
|
||||||
this.$t('profile.updateError')
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
this.loadProfile()
|
this.loadProfile();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.profile-container {
|
.profile-container {
|
||||||
max-width: 800px;
|
max-width: 1200px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 1rem;
|
padding: 40px 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-subtitle {
|
||||||
|
color: #7f8c8d;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.profile-form {
|
||||||
|
background: #fff;
|
||||||
|
border-radius: 15px;
|
||||||
|
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-grid {
|
.profile-grid {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 1fr 2fr;
|
grid-template-columns: 300px 1fr;
|
||||||
gap: 1rem;
|
gap: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-photo-wrapper {
|
.photo-section {
|
||||||
|
padding: 30px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-right: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-section {
|
||||||
|
padding: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-wrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
text-align: center;
|
width: 200px;
|
||||||
|
height: 200px;
|
||||||
|
margin: 0 auto 20px;
|
||||||
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.profile-photo {
|
.profile-photo {
|
||||||
width: 200px;
|
width: 100%;
|
||||||
height: 200px;
|
height: 100%;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
border-radius: 50%;
|
transition: transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.photo-upload-overlay {
|
.photo-overlay {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 0;
|
top: 0;
|
||||||
left: 50%;
|
left: 0;
|
||||||
transform: translateX(-50%);
|
width: 100%;
|
||||||
background: rgba(0,0,0,0.5);
|
height: 100%;
|
||||||
color: white;
|
background: rgba(0, 0, 0, 0.5);
|
||||||
padding: 0.5rem;
|
display: flex;
|
||||||
border-radius: 0 0 100px 100px;
|
align-items: center;
|
||||||
cursor: pointer;
|
justify-content: center;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden-file-input {
|
.photo-text {
|
||||||
|
color: white;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-wrapper:hover .photo-overlay {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-wrapper:hover .profile-photo {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin: 30px 0 20px;
|
||||||
|
padding-bottom: 10px;
|
||||||
|
border-bottom: 2px solid #e1e8ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title i {
|
||||||
|
color: #3498db;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title h2 {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
color: #2c3e50;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: 20px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
color: #34495e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 2px solid #e1e8ed;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 14px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input:focus,
|
||||||
|
.form-select:focus {
|
||||||
|
border-color: #3498db;
|
||||||
|
box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input.readonly {
|
||||||
|
background-color: #f8f9fa;
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: #7f8c8d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-input {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-field {
|
.upload-btn {
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field label {
|
|
||||||
display: block;
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-field input,
|
|
||||||
.form-field select {
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0.5rem;
|
padding: 12px;
|
||||||
border: 1px solid #ccc;
|
background: #3498db;
|
||||||
border-radius: 4px;
|
color: white;
|
||||||
}
|
|
||||||
|
|
||||||
.is-invalid {
|
|
||||||
border-color: #dc3545;
|
|
||||||
}
|
|
||||||
|
|
||||||
.validation-error {
|
|
||||||
color: #dc3545;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 1rem;
|
|
||||||
margin-top: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn {
|
|
||||||
padding: 0.5rem 1rem;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 4px;
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: background 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-primary {
|
.upload-btn:hover {
|
||||||
background-color: #007bff;
|
background: #2980b9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding: 30px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-top: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn,
|
||||||
|
.cancel-btn {
|
||||||
|
padding: 12px 24px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn {
|
||||||
|
background: #2ecc71;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.save-btn:hover {
|
||||||
background-color: #6c757d;
|
background: #27ae60;
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cancel-btn {
|
||||||
|
background: #e74c3c;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn:disabled {
|
.cancel-btn:hover {
|
||||||
opacity: 0.5;
|
background: #c0392b;
|
||||||
cursor: not-allowed;
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.profile-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.photo-section {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button-group {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
|
||||||
|
.save-btn,
|
||||||
|
.cancel-btn {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
Loading…
Reference in New Issue
Block a user