add edit profile page & footer

This commit is contained in:
mi1468 2025-05-03 14:32:18 +03:30
parent 56769fc1b8
commit 34324f447c
8 changed files with 549 additions and 194 deletions

View File

@ -2,17 +2,24 @@
<div id="app">
<!-- Main App Layout -->
<h1 class="app-title"> </h1>
<!-- The router-view here will display the active route's component -->
<router-view></router-view>
</div>
<Footer />
</template>
<script>
import Footer from '@/components/Footer.vue'
export default {
name: 'App',
components: {
Footer
}
}
</script>
@ -58,14 +65,14 @@ router-view {
<style>
@font-face {
font-family: 'Yekan';
src: url('@/assets/fonts/Yekan.ttf') format('truetype');
font-family: 'IRANSans';
src: url('@/assets/fonts/IRANSansXFaNum-Medium.ttf') format('truetype');
font-weight: normal;
font-style: normal;
}
/* Apply the font globally */
body {
font-family: 'Yekan', sans-serif;
* {
font-family: 'IRANSans', sans-serif !important;
}
</style>

View File

@ -0,0 +1,31 @@
<template>
<footer class="footer">
<div class="text-wrapper-13">All Rights Reserved ©Dadechin</div>
</footer>
</template>
<script>
export default {
name: 'AppFooter'
}
</script>
<style scoped>
.footer {
/* margin-top: 48px; */
text-align: center;
color: #aaa;
font-size: 12px;
padding: 20px 0;
background-color: #101010; /* Optional: Add a light background */
}
.clip-path-group {
width: auto;
}
</style>

View File

@ -3,16 +3,26 @@
<div class="sidebar">
<div class="group">
<!-- Profile Info -->
<div class="overlap">
<img class="profile" src="https://c.animaapp.com/m9nvumalUMfQbN/img/profile.png" />
<div class="frame-2">
<div class="text-wrapper-2">خوش آمدید...</div>
<div class="text-wrapper-3">دانیال پژوهش کیا</div>
<div class="logo-xroom">
<div class="logo">
<div class="clip-path-group-wrapper">
<img class="clip-path-group" src="https://c.animaapp.com/m9nvumalUMfQbN/img/clip-path-group.png" />
</div>
</div>
</div>
<div class="notifications">
<div class="overlap-group"><div class="text-wrapper-4">4</div></div>
</div>
<router-link to="/dashboard/edit-profile" class="profile-link">
<div class="profile-container">
<img class="profile" src="https://c.animaapp.com/m9nvumalUMfQbN/img/profile.png" />
<div class="frame-2">
<div class="text-wrapper-2">خوش آمدید...</div>
<div class="text-wrapper-3">دانیال پژوهش کیا</div>
</div>
<div class="notifications">
<div class="notification-badge">4</div>
</div>
</div>
</router-link>
</div>
<!-- Menu -->
@ -53,6 +63,9 @@
</router-link>
</div>
</div>
</template>
<script>
@ -60,9 +73,9 @@ export default {
name: 'SidebarMenu',
methods: {
isActive(path) {
return this.$route.path === path
return this.$route.path === path
}
},
},
data() {
return {
activeMenu: 'dashboard'
@ -72,6 +85,127 @@ export default {
</script>
<style scoped>
.footer {
margin-top: 48px;
text-align: center;
color: #aaa;
font-size: 12px;
}
.sidebar {
background-color: #101010;
width: 360px;
height: 100vh;
position: fixed;
right: 0;
top: 0;
padding: 30px 50px;
direction: rtl;
display: flex;
flex-direction: column;
}
.group {
width: 228px;
margin-bottom: 75px;
}
.logo-xroom {
display: flex;
flex-direction: column;
align-items: center;
margin-bottom: 20px;
}
.text-wrapper-13 {
font-family: "IRANSansXFaNum-Medium", Helvetica;
color: #e6e6e6;
font-size: 12px;
margin-top: 10px;
}
.logo {
display: flex;
justify-content: center;
}
.clip-path-group-wrapper {
width: 100px;
height: 100px;
}
.clip-path-group {
width: 100%;
height: 100%;
object-fit: contain;
}
.profile-link {
text-decoration: none;
color: inherit;
}
.profile-container {
display: flex;
align-items: center;
position: relative;
padding: 10px 0;
}
.profile {
width: 72px;
height: 72px;
margin-left: 20px;
}
.frame-2 {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 8px;
}
.text-wrapper-2 {
font-family: "IRANSansXFaNum-Medium", Helvetica;
font-weight: 500;
color: #e6e6e6;
font-size: 16px;
line-height: 22.4px;
}
.text-wrapper-3 {
font-family: "IRANSansXFaNum-Medium", Helvetica;
font-weight: 500;
color: white;
font-size: 19px;
line-height: 26.6px;
}
.notifications {
position: absolute;
left: 0;
top: 10px;
}
.notification-badge {
display: flex;
align-items: center;
justify-content: center;
width: 25px;
height: 25px;
background-color: #dc3434;
border-radius: 50%;
font-family: "IRANSansXFaNum-DemiBold", Helvetica;
font-weight: 700;
color: #ffffff;
font-size: 13px;
}
.frame {
display: flex;
flex-direction: column;
width: 260px;
gap: 4px;
}
.nav-button {
all: unset;
@ -99,172 +233,17 @@ export default {
background-color: #3a57e8;
}
.sidebar {
background-color: #101010;
width: 360px;
height: 100vh;
position: fixed;
right: 0;
top: 0;
padding: 30px 50px;
direction: rtl;
}
.group {
position: relative;
width: 228px;
height: 88px;
margin-bottom: 75px;
}
.overlap {
position: absolute;
width: 137px;
height: 72px;
top: 16px;
right: 91px;
}
.profile {
position: absolute;
width: 72px;
height: 72px;
top: 0;
right: -95px;
}
.frame-2 {
display: inline-flex;
flex-direction: column;
align-items: flex-start;
justify-content: right;
gap: 8px;
position: absolute;
top: 9px;
right: 0;
}
.text-wrapper-2 {
position: relative;
width: fit-content;
margin-top: -1px;
font-family: "IRANSansXFaNum-Medium", Helvetica;
font-weight: 500;
color: #e6e6e6;
font-size: 16px;
text-align: right;
line-height: 22.4px;
white-space: nowrap;
letter-spacing: 0;
}
.text-wrapper-3 {
position: relative;
width: fit-content;
font-family: "IRANSansXFaNum-Medium", Helvetica;
font-weight: 500;
color: white;
font-size: 19px;
text-align: right;
line-height: 26.6px;
white-space: nowrap;
letter-spacing: 0;
}
.notifications {
position: absolute;
width: 31px;
height: 29px;
top: 0;
right: 52px;
}
.overlap-group {
position: relative;
height: 31px;
top: 1px;
right: -1px;
background-color: #dc3434;
border-radius: 15.5px;
}
.text-wrapper-4 {
position: absolute;
top: 9px;
right: 11px;
font-family: "IRANSansXFaNum-DemiBold", Helvetica;
font-weight: 700;
color: #ffffff;
font-size: 13px;
letter-spacing: 0.22px;
line-height: normal;
white-space: nowrap;
}
.frame {
display: flex;
flex-direction: column;
width: 260px;
align-items: flex-start;
justify-content: center;
gap: 4px;
}
.BTN {
all: unset;
box-sizing: border-box;
display: flex;
width: 260px;
height: 57px;
align-items: center;
justify-content: flex-start;
gap: 10px;
padding: 16px 24px;
position: relative;
background-color: #3a57e8;
border-radius: 10px;
cursor: pointer;
}
.text-wrapper {
position: relative;
width: fit-content;
margin-top: -2px;
font-family: "IRANSansXFaNum-Medium", Helvetica;
font-weight: 500;
color: white;
font-size: 18px;
text-align: right;
letter-spacing: 0;
line-height: 25.2px;
white-space: nowrap;
}
.img {
position: relative;
width: 24px;
height: 24px;
}
.button {
all: unset;
box-sizing: border-box;
display: flex;
width: 260px;
height: 57px;
align-items: center;
justify-content: flex-start;
gap: 10px;
padding: 16px 24px;
position: relative;
background-color: #101010;
border-radius: 10px;
cursor: pointer;
}
.button:hover {
background-color: #1e1e1e;
}
</style>

View File

@ -80,9 +80,7 @@ export default {
</script>
<style scoped>
/* Add your styles here */
</style>
<style scoped>

View File

@ -0,0 +1,321 @@
<template>
<SidebarMenu />
<div class="dashboard-page">
<div class="content">
<!-- Header -->
<div class="header-row">
<div class="right-actions">
<button class="subscription-button">
<img src="https://c.animaapp.com/m9nvumalUMfQbN/img/frame-6.svg" class="button-icon" />
خرید اشتراک
</button>
</div>
<div class="user-info">
<span class="user-name">{{ userData.user.first_name }} {{ userData.user.last_name }}</span>
<div class="avatar-box">
<img class="avatar-icon" src="https://c.animaapp.com/m9nvumalUMfQbN/img/frame.svg" />
</div>
</div>
</div>
<div class="page-title">ویرایش پروفایل</div>
<!-- Two-Column Form Layout -->
<div class="profile-edit-container">
<!-- Left Column -->
<div class="column">
<!-- VR Avatar Section -->
<div class="form-section">
<h3>آواتار واقعیت مجازی شما</h3>
<p class="section-description">
میتوانید با آپلود یک تصویر، به شخصیسازی آواتار خود، ظاهر خود را در محیط واقعیت مجازی ویرایش کنید.
</p>
<img :src="userAvatarUrl" class="avatar-image" />
<div class="avatar-actions">
<a @click="changeAvatar">تغییر آواتار</a> |
<a @click="regenerateAvatar">ساخت مجدد آواتار</a>
</div>
</div>
<!-- Password Change Section -->
<div class="form-section">
<h3>تغییر رمز عبور</h3>
<div class="form-group">
<label for="currentPassword">رمز عبور فعلی</label>
<input type="password" id="currentPassword" v-model="passwordForm.current_password" />
</div>
<div class="form-group">
<label for="newPassword">رمز عبور جدید</label>
<input type="password" id="newPassword" v-model="passwordForm.new_password" />
</div>
<div class="form-group">
<label for="confirmPassword">تأیید رمز عبور جدید</label>
<input type="password" id="confirmPassword" v-model="passwordForm.confirm_password" />
</div>
<button class="save-btn" @click="saveProfile" :disabled="saving">
{{ saving ? 'در حال ذخیره...' : 'ذخیره' }}
</button>
</div>
</div>
<!-- Right Column -->
<div class="column">
<!-- Profile Picture Section -->
<div class="form-section">
<h3>تصویر پروفایل</h3>
<p class="section-description">
این نماد در کنار نام شما و برای دیگران در واقعیت مجازی و در پلتفرم وب قابل مشاهده خواهد بود.
</p>
<img :src="userProfilePicUrl" class="profile-image" />
<input type="file" @change="uploadProfileImage" class="upload-input" />
</div>
<!-- User Info Section -->
<div class="form-section">
<h3>اطلاعات کاربر</h3>
<div class="form-group">
<label for="email">آدرس ایمیل</label>
<input type="email" id="email" v-model="editForm.email" disabled />
</div>
<div class="form-group">
<label for="firstName">نام و نام خانوادگی</label>
<input type="text" id="firstName" v-model="editForm.first_name" />
<input type="text" id="lastName" v-model="editForm.last_name" />
</div>
<div class="form-group">
<label for="position">جایگاه</label>
<input type="text" id="position" placeholder="جایگاه شغلی (اختیاری)" />
</div>
<button class="save-btn" @click="saveProfile" :disabled="saving">
{{ saving ? 'در حال ذخیره...' : 'ذخیره' }}
</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import SidebarMenu from '@/components/SidebarMenu.vue'
import axios from '@/axios';
export default {
name: 'EditProfile',
components: {
SidebarMenu
},
data() {
return {
selectedProfileImage: null,
userData: {
user: { first_name: '', last_name: '', email: '' }
},
editForm: { first_name: '', last_name: '', email: '' },
passwordForm: { current_password: '', new_password: '', confirm_password: '' },
saving: false,
userProfilePicUrl: 'https://i.imgur.com/QbXfV6C.png',
userAvatarUrl: 'https://i.imgur.com/QbXfV6C.png',
baseUrl: 'http://194.62.43.230:8000'
}
},
created() {
this.fetchUserData();
},
methods: {
async fetchUserData() {
try {
const response = await axios.get('/getInfo');
this.userData = response.data;
this.editForm = {
first_name: response.data.user.first_name,
last_name: response.data.user.last_name,
email: response.data.user.email,
userAvatarUrl: this.baseUrl+ "/"+ response.data.customer.profile_img
};
} catch (error) {
console.error('Error fetching user data:', error);
}
},
async saveProfile() {
this.saving = true;
try {
const formData = new FormData();
formData.append('first_name', this.editForm.first_name);
formData.append('last_name', this.editForm.last_name);
if (this.selectedProfileImage) {
formData.append('profile_img', this.selectedProfileImage);
}
await axios.post(`${this.baseUrl}/editProfile/`, formData, {
headers: {
'Content-Type': 'multipart/form-data'
}
});
// Handle password change if filled
if (this.passwordForm.new_password && this.passwordForm.current_password) {
if (this.passwordForm.new_password !== this.passwordForm.confirm_password) {
throw new Error('رمز عبور جدید و تکرار آن مطابقت ندارند');
}
await axios.post(`${this.baseUrl}/resetPassword/`, {
old_password: this.passwordForm.current_password,
new_password: this.passwordForm.new_password
});
}
await this.fetchUserData();
alert('تغییرات با موفقیت ذخیره شد');
} catch (error) {
alert(error.response?.data?.detail || error.message || 'خطا در ذخیره تغییرات');
} finally {
this.saving = false;
}
},
changeAvatar() {
alert('تغییر آواتار کلیک شد');
},
regenerateAvatar() {
alert('ساخت مجدد آواتار کلیک شد');
},
uploadProfileImage(event) {
const file = event.target.files[0];
if (file) {
this.selectedProfileImage = file;
this.userProfilePicUrl = URL.createObjectURL(file);
}
}
}
}
</script>
<style scoped>
.dashboard-page {
margin-right: 360px;
padding: 20px;
direction: rtl;
font-family: IRANSansXFaNum, sans-serif;
}
.content {
background-color: #f8f9fa;
border-radius: 20px;
padding: 35px 80px;
display: flex;
flex-direction: column;
gap: 32px;
}
.page-title {
font-size: 22px;
font-weight: bold;
margin: 24px 0;
color: #333;
}
.profile-edit-container {
display: flex;
gap: 32px;
flex-direction: row-reverse;
}
.column {
flex: 1;
}
.form-section {
background: #fff;
border-radius: 12px;
padding: 24px;
margin-bottom: 24px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
}
.form-section h3 {
margin-bottom: 8px;
color: #222;
font-size: 18px;
}
.section-description {
color: #777;
font-size: 14px;
margin-bottom: 12px;
}
.form-group {
margin-bottom: 16px;
}
.form-group label {
display: block;
margin-bottom: 6px;
color: #444;
font-weight: 500;
}
input[type="text"],
input[type="email"],
input[type="password"] {
width: 100%;
padding: 10px 14px;
font-size: 14px;
border: 1px solid #ddd;
border-radius: 8px;
direction: rtl;
}
input:disabled {
background: #f5f5f5;
color: #777;
}
.avatar-image,
.profile-image {
width: 80px;
height: 80px;
border-radius: 50%;
margin-bottom: 12px;
}
.avatar-actions a {
color: #3a57e8;
font-size: 14px;
cursor: pointer;
margin: 0 4px;
}
.save-btn {
background: #3a57e8;
color: #fff;
border: none;
padding: 10px 24px;
border-radius: 8px;
font-weight: 500;
cursor: pointer;
margin-top: 8px;
}
.save-btn:disabled {
background-color: #ccc;
cursor: not-allowed;
}
.upload-input {
margin-top: 8px;
}
</style>

View File

@ -1,29 +1,38 @@
import { createRouter, createWebHistory } from 'vue-router'
import SignupPage from '../pages/SignupPage.vue' // Renamed
import LoginPage from '../pages/LoginPage.vue' // Renamed
import SignupPage from '../pages/SignupPage.vue'
import LoginPage from '../pages/LoginPage.vue'
import DashboardPage from '../pages/dashboard/index.vue'
import FilesPage from '@/pages/dashboard/files.vue'; // import the new page
import FilesPage from '@/pages/dashboard/files.vue';
import axios from '@/axios';
const routes = [
{
path: '/signup',
name: 'SignupPage', // Renamed
name: 'SignupPage',
component: SignupPage
},
{
path: '/login',
name: 'LoginPage', // Renamed
name: 'LoginPage',
component: LoginPage
},
{
path: '/dashboard',
name: 'DashboardPage', // Renamed
component: DashboardPage
},{
name: 'DashboardPage',
component: DashboardPage,
meta: { requiresAuth: true }
},
{
path: '/dashboard/files',
name: 'files',
component: FilesPage, // link the files page
component: FilesPage,
meta: { requiresAuth: true }
},
{
path: '/dashboard/edit-profile',
name: 'EditProfile',
component: () => import('@/pages/dashboard/EditProfile.vue'),
meta: { requiresAuth: true }
}
]
@ -32,24 +41,35 @@ const router = createRouter({
routes
})
router.beforeEach(async (to, from, next) => {
const token = localStorage.getItem('token');
// No token, redirect to login if trying to access dashboard
if (to.path === '/dashboard' && !token) {
// Check if the route requires authentication
const requiresAuth = to.matched.some(record => record.meta.requiresAuth);
// If route doesn't require auth, continue
if (!requiresAuth) {
return next();
}
// If route requires auth but no token, redirect to login
if (requiresAuth && !token) {
return next('/login');
}
// If we have a token and it's an auth route, verify it
if (token) {
try {
await axios.get('/getInfo');
// If trying to access login page while authenticated, redirect to dashboard
if (to.path === '/login') {
return next('/dashboard');
}
return next();
} catch (err) {
// Invalid token, redirect to login
// Invalid token, clear storage and redirect to login
localStorage.removeItem('token');
localStorage.removeItem('user');
return next('/login');
@ -59,5 +79,4 @@ router.beforeEach(async (to, from, next) => {
next();
});
export default router