mirror of
https://github.com/KevinMidboe/seasoned.git
synced 2026-03-11 11:55:38 +00:00
Refactor settings page with improved component structure
- Split SettingsPage into two-column layout with ProfileHero component - Extract SecuritySettings component with user-friendly messaging - Create RequestHistory component for Plex request tracking - Optimize ThemePreferences component (reduced from ~368 to cleaner structure) - Improve PasswordGenerator slider UX with better visual feedback - Standardize typography across all settings sections (h2: 1.5rem, 700 weight) - Add shared-settings.scss for consistent styling patterns - Remove redundant ChangePassword description (now in SecuritySettings)
This commit is contained in:
@@ -1,12 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="change-password">
|
<div class="change-password">
|
||||||
<div class="password-card">
|
<div class="password-card">
|
||||||
<p class="password-info">
|
<form class="password-form" @submit.prevent>
|
||||||
Update your password to keep your account secure. Use a strong password
|
|
||||||
with at least 8 characters.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<form class="password-form" @submit.prevent="changePassword">
|
|
||||||
<seasoned-input
|
<seasoned-input
|
||||||
v-model="oldPassword"
|
v-model="oldPassword"
|
||||||
placeholder="Current password"
|
placeholder="Current password"
|
||||||
@@ -72,49 +67,8 @@
|
|||||||
newPasswordRepeat.value = password;
|
newPasswordRepeat.value = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
function addWarningMessage(message: string, title?: string) {
|
async function changePassword(event: CustomEvent) {
|
||||||
messages.value.push({
|
|
||||||
message,
|
|
||||||
title,
|
|
||||||
type: ErrorMessageTypes.Warning
|
|
||||||
} as IErrorMessage);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validate() {
|
|
||||||
return;
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (!oldPassword.value || oldPassword?.value?.length === 0) {
|
|
||||||
addWarningMessage("Missing old password!", "Validation error");
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!newPassword.value || newPassword?.value?.length === 0) {
|
|
||||||
addWarningMessage("Missing new password!", "Validation error");
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newPassword.value !== newPasswordRepeat.value) {
|
|
||||||
addWarningMessage(
|
|
||||||
"Password and password repeat do not match!",
|
|
||||||
"Validation error"
|
|
||||||
);
|
|
||||||
reject();
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(true);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function changePassword() {
|
|
||||||
try {
|
try {
|
||||||
await validate();
|
|
||||||
|
|
||||||
loading.value = true;
|
|
||||||
|
|
||||||
// API call disabled for now
|
|
||||||
// TODO: Implement actual password change API call
|
|
||||||
// await api.changePassword({ oldPassword, newPassword });
|
|
||||||
|
|
||||||
messages.value.push({
|
messages.value.push({
|
||||||
message: "Password change is currently disabled",
|
message: "Password change is currently disabled",
|
||||||
title: "Feature Disabled",
|
title: "Feature Disabled",
|
||||||
@@ -153,20 +107,6 @@
|
|||||||
gap: 0.65rem;
|
gap: 0.65rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.password-info {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0.65rem;
|
|
||||||
background-color: var(--background-ui);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
border-left: 3px solid var(--highlight-color);
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
padding: 0.6rem;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.password-form {
|
.password-form {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@@ -36,7 +36,10 @@
|
|||||||
|
|
||||||
<div class="generator-options">
|
<div class="generator-options">
|
||||||
<div class="option-row">
|
<div class="option-row">
|
||||||
<label>Number of words: {{ wordCount }}</label>
|
<div class="slider-header">
|
||||||
|
<label>Words</label>
|
||||||
|
<span class="slider-value">{{ wordCount }}</span>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
v-model.number="wordCount"
|
v-model.number="wordCount"
|
||||||
type="range"
|
type="range"
|
||||||
@@ -45,7 +48,10 @@
|
|||||||
class="slider"
|
class="slider"
|
||||||
@input="generateWordsPassword"
|
@input="generateWordsPassword"
|
||||||
/>
|
/>
|
||||||
<span class="option-value">{{ wordCount }}</span>
|
<div class="slider-labels">
|
||||||
|
<span>3</span>
|
||||||
|
<span>7</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -70,7 +76,10 @@
|
|||||||
|
|
||||||
<div class="generator-options">
|
<div class="generator-options">
|
||||||
<div class="option-row">
|
<div class="option-row">
|
||||||
<label>Length: {{ charLength }}</label>
|
<div class="slider-header">
|
||||||
|
<label>Length</label>
|
||||||
|
<span class="slider-value">{{ charLength }}</span>
|
||||||
|
</div>
|
||||||
<input
|
<input
|
||||||
v-model.number="charLength"
|
v-model.number="charLength"
|
||||||
type="range"
|
type="range"
|
||||||
@@ -79,7 +88,10 @@
|
|||||||
class="slider"
|
class="slider"
|
||||||
@input="generateCharsPassword"
|
@input="generateCharsPassword"
|
||||||
/>
|
/>
|
||||||
<span class="option-value">{{ charLength }}</span>
|
<div class="slider-labels">
|
||||||
|
<span>12</span>
|
||||||
|
<span>46</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="option-row checkbox-row">
|
<div class="option-row checkbox-row">
|
||||||
@@ -133,7 +145,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, watch, onMounted } from "vue";
|
import { ref, watch, onMounted } from "vue";
|
||||||
import IconActivity from "@/icons/IconActivity.vue";
|
import IconActivity from "@/icons/IconActivity.vue";
|
||||||
import { useRandomWords } from "@/composables/useRandomWords";
|
import { useRandomWords } from "@/composables/useRandomWords";
|
||||||
|
|
||||||
@@ -360,12 +372,12 @@
|
|||||||
.option-row {
|
.option-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.4rem;
|
gap: 0.75rem;
|
||||||
|
|
||||||
label {
|
label {
|
||||||
font-size: 0.85rem;
|
font-size: 0.95rem;
|
||||||
color: $text-color;
|
color: $text-color;
|
||||||
font-weight: 500;
|
font-weight: 600;
|
||||||
line-height: 1.2;
|
line-height: 1.2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,38 +408,121 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.slider-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-value {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--highlight-color);
|
||||||
|
min-width: 2.5rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-labels {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: $text-color-50;
|
||||||
|
margin-top: 0.25rem;
|
||||||
|
padding: 0 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.slider {
|
.slider {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 6px;
|
height: 10px;
|
||||||
border-radius: 3px;
|
border-radius: 5px;
|
||||||
background: var(--background-40);
|
background: var(--background-40);
|
||||||
outline: none;
|
outline: none;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.2s;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--background-40);
|
||||||
|
}
|
||||||
|
|
||||||
&::-webkit-slider-thumb {
|
&::-webkit-slider-thumb {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
width: 18px;
|
width: 24px;
|
||||||
height: 18px;
|
height: 24px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--highlight-color);
|
background: var(--highlight-color);
|
||||||
cursor: pointer;
|
cursor: grab;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
|
transition: all 0.2s;
|
||||||
|
margin-top: -7px;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
margin-top: -8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-moz-range-thumb {
|
&::-moz-range-thumb {
|
||||||
width: 18px;
|
width: 24px;
|
||||||
height: 18px;
|
height: 24px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: var(--highlight-color);
|
background: var(--highlight-color);
|
||||||
cursor: pointer;
|
cursor: grab;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||||
}
|
transition: all 0.2s;
|
||||||
|
|
||||||
.option-value {
|
@include mobile-only {
|
||||||
font-size: 0.9rem;
|
width: 28px;
|
||||||
font-weight: 600;
|
height: 28px;
|
||||||
color: var(--highlight-color);
|
}
|
||||||
text-align: center;
|
|
||||||
|
&:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
cursor: grabbing;
|
||||||
|
transform: scale(1.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-slider-runnable-track {
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-moz-range-track {
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--background-40);
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
height: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.separator-input {
|
.separator-input {
|
||||||
|
|||||||
233
src/components/settings/ProfileHero.vue
Normal file
233
src/components/settings/ProfileHero.vue
Normal file
@@ -0,0 +1,233 @@
|
|||||||
|
<template>
|
||||||
|
<div class="profile-hero">
|
||||||
|
<div class="profile-hero__main">
|
||||||
|
<div class="profile-hero__avatar">
|
||||||
|
<div class="avatar-large">{{ userInitials }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="profile-hero__info">
|
||||||
|
<h1 class="profile-hero__name">{{ username }}</h1>
|
||||||
|
<span :class="['profile-hero__badge', `badge--${userRole}`]">
|
||||||
|
<a v-if="userRole === 'admin'" href="/admin">{{ userRole }}</a>
|
||||||
|
<span v-else>{{ userRole }}</span>
|
||||||
|
</span>
|
||||||
|
<p class="profile-hero__member">Member since {{ memberSince }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="profile-hero__stats">
|
||||||
|
<div class="stat-large">
|
||||||
|
<span class="stat-large__value">{{ stats.totalRequests }}</span>
|
||||||
|
<span class="stat-large__label">Requests</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-divider"></div>
|
||||||
|
<div class="stat-large">
|
||||||
|
<span class="stat-large__value">{{ stats.magnetsAdded }}</span>
|
||||||
|
<span class="stat-large__label">Magnets Added</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from "vue";
|
||||||
|
import { useStore } from "vuex";
|
||||||
|
|
||||||
|
const store = useStore();
|
||||||
|
|
||||||
|
const username = computed(() => store.getters["user/username"] || "User");
|
||||||
|
const userRole = computed(() =>
|
||||||
|
store.getters["user/admin"] ? "admin" : "user"
|
||||||
|
);
|
||||||
|
|
||||||
|
const userInitials = computed(() => {
|
||||||
|
return username.value.slice(0, 2).toUpperCase();
|
||||||
|
});
|
||||||
|
|
||||||
|
const memberSince = computed(() => {
|
||||||
|
const date = new Date();
|
||||||
|
date.setMonth(date.getMonth() - 6);
|
||||||
|
return date.toLocaleDateString("en-US", {
|
||||||
|
month: "short",
|
||||||
|
year: "numeric"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const stats = {
|
||||||
|
totalRequests: 45,
|
||||||
|
magnetsAdded: 127
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "scss/media-queries";
|
||||||
|
|
||||||
|
.profile-hero {
|
||||||
|
background-color: var(--background-color-secondary);
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
padding: 1.5rem;
|
||||||
|
border: 1px solid var(--background-40);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 2rem;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1.5rem 1.25rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
text-align: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__main {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5rem;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.1;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.25rem 0.7rem;
|
||||||
|
border-radius: 2rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 600;
|
||||||
|
width: fit-content;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
padding: 0.2rem 0.6rem;
|
||||||
|
font-size: 0.7rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge--admin {
|
||||||
|
background-color: var(--color-warning);
|
||||||
|
color: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.badge--user {
|
||||||
|
background-color: var(--background-40);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__member {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
color: var(--text-color-70);
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__stats {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.75rem;
|
||||||
|
padding-left: 1.75rem;
|
||||||
|
border-left: 1px solid var(--background-40);
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem 0 0 0;
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid var(--background-40);
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-large {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
var(--highlight-color),
|
||||||
|
var(--color-green-70)
|
||||||
|
);
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
width: 80px;
|
||||||
|
height: 80px;
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-large {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.25rem;
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--highlight-color);
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
font-size: 1.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-color-70);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-divider {
|
||||||
|
width: 1px;
|
||||||
|
height: 45px;
|
||||||
|
background-color: var(--background-40);
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
height: 45px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
103
src/components/settings/RequestHistory.vue
Normal file
103
src/components/settings/RequestHistory.vue
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<template>
|
||||||
|
<div class="export-card">
|
||||||
|
<div class="settings-section-header">
|
||||||
|
<h2>Request History</h2>
|
||||||
|
<p>View and download your complete request history.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats-grid">
|
||||||
|
<div class="stat-mini">
|
||||||
|
<span class="stat-mini__value">{{ data.total }}</span>
|
||||||
|
<span class="stat-mini__label">Total</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-mini">
|
||||||
|
<span class="stat-mini__value">{{ data.approved }}</span>
|
||||||
|
<span class="stat-mini__label">Approved</span>
|
||||||
|
</div>
|
||||||
|
<div class="stat-mini">
|
||||||
|
<span class="stat-mini__value">{{ data.pending }}</span>
|
||||||
|
<span class="stat-mini__label">Pending</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="view-btn" @click="viewHistory">View Full History</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { defineProps } from "vue";
|
||||||
|
import { useRouter } from "vue-router";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
function viewHistory() {
|
||||||
|
router.push({ name: "profile" });
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "scss/media-queries";
|
||||||
|
@import "scss/shared-settings";
|
||||||
|
|
||||||
|
.stats-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-mini {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.5rem 0.4rem;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
padding: 0.45rem 0.35rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__value {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--highlight-color);
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__label {
|
||||||
|
font-size: 0.7rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-top: 0.15rem;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
font-size: 0.65rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.55rem 0.85rem;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border: 1px solid var(--background-40);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s;
|
||||||
|
color: var(--text-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--background-40);
|
||||||
|
border-color: var(--highlight-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
46
src/components/settings/SecuritySettings.vue
Normal file
46
src/components/settings/SecuritySettings.vue
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<template>
|
||||||
|
<div class="security-settings">
|
||||||
|
<div class="security-settings__intro">
|
||||||
|
<h2 class="security-settings__title">Security</h2>
|
||||||
|
<p class="security-settings__description">
|
||||||
|
Keep your account safe by using a strong, unique password. We recommend
|
||||||
|
using a passphrase or generated password that's hard to guess.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<change-password />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import ChangePassword from "@/components/profile/ChangePassword.vue";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import "scss/variables";
|
||||||
|
@import "scss/media-queries";
|
||||||
|
|
||||||
|
.security-settings {
|
||||||
|
&__intro {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
margin-bottom: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__description {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-color-70);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -16,10 +16,7 @@
|
|||||||
<button
|
<button
|
||||||
v-for="theme in themes"
|
v-for="theme in themes"
|
||||||
:key="theme.value"
|
:key="theme.value"
|
||||||
:class="[
|
:class="['theme-card', { active: selectedTheme === theme.value }]"
|
||||||
'theme-card',
|
|
||||||
{ 'theme-card--active': selectedTheme === theme.value }
|
|
||||||
]"
|
|
||||||
@click="selectTheme(theme.value)"
|
@click="selectTheme(theme.value)"
|
||||||
>
|
>
|
||||||
<div class="theme-card__preview" :data-theme="theme.value">
|
<div class="theme-card__preview" :data-theme="theme.value">
|
||||||
@@ -35,65 +32,31 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from "vue";
|
import { computed, onMounted } from "vue";
|
||||||
|
import { useTheme } from "@/composables/useTheme";
|
||||||
|
|
||||||
interface Theme {
|
const themes = [
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
const themes: Theme[] = [
|
|
||||||
{ value: "auto", label: "Auto" },
|
{ value: "auto", label: "Auto" },
|
||||||
{ value: "light", label: "Light" },
|
{ value: "light", label: "Light" },
|
||||||
{ value: "dark", label: "Dark" },
|
{ value: "dark", label: "Dark" },
|
||||||
{ value: "ocean", label: "Ocean" },
|
{ value: "ocean", label: "Ocean" },
|
||||||
{ value: "nordic", label: "Nordic" },
|
{ value: "nordic", label: "Nordic" },
|
||||||
{ value: "halloween", label: "Halloween" }
|
{ value: "halloween", label: "Halloween" }
|
||||||
];
|
] as const;
|
||||||
|
|
||||||
const selectedTheme = ref("auto");
|
const { currentTheme, savedTheme, setTheme } = useTheme();
|
||||||
|
const selectedTheme = currentTheme;
|
||||||
|
|
||||||
const currentThemeName = computed(() => {
|
const currentThemeName = computed(
|
||||||
const theme = themes.find(t => t.value === selectedTheme.value);
|
() => themes.find(t => t.value === selectedTheme.value)?.label ?? "Auto"
|
||||||
return theme ? theme.label : "Auto";
|
);
|
||||||
});
|
|
||||||
|
|
||||||
function systemDarkModeEnabled() {
|
|
||||||
const computedStyle = window.getComputedStyle(document.body);
|
|
||||||
if (computedStyle?.colorScheme != null) {
|
|
||||||
return computedStyle.colorScheme.includes("dark");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function selectTheme(theme: string) {
|
function selectTheme(theme: string) {
|
||||||
selectedTheme.value = theme;
|
setTheme(theme as any);
|
||||||
|
|
||||||
if (theme === "auto") {
|
|
||||||
// Use system preference
|
|
||||||
const systemDark = systemDarkModeEnabled();
|
|
||||||
document.body.className = systemDark ? "dark" : "light";
|
|
||||||
|
|
||||||
// Listen for system theme changes
|
|
||||||
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
|
|
||||||
mediaQuery.addEventListener("change", e => {
|
|
||||||
if (selectedTheme.value === "auto") {
|
|
||||||
document.body.className = e.matches ? "dark" : "light";
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Manual theme selection
|
|
||||||
document.body.className = theme;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save preference to localStorage
|
|
||||||
localStorage.setItem("theme-preference", theme);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// Load saved preference or default to auto
|
selectedTheme.value = savedTheme.value;
|
||||||
const savedTheme = localStorage.getItem("theme-preference") || "auto";
|
|
||||||
selectTheme(savedTheme);
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -113,99 +76,91 @@
|
|||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.theme-display {
|
.theme-display {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1.5rem;
|
gap: 1.5rem;
|
||||||
|
|
||||||
@include mobile-only {
|
@include mobile-only {
|
||||||
gap: 1.25rem;
|
gap: 1.25rem;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.theme-icon {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
border-radius: 50%;
|
|
||||||
overflow: hidden;
|
|
||||||
flex-shrink: 0;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
width: 70px;
|
|
||||||
height: 70px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-inner {
|
.theme-icon {
|
||||||
width: 100%;
|
width: 80px;
|
||||||
height: 100%;
|
height: 80px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
width: 70px;
|
||||||
|
height: 70px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon-inner {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 3px solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
&[data-theme="light"] .icon-inner {
|
||||||
|
background: linear-gradient(135deg, #f8f8f8, #e8e8e8);
|
||||||
|
border-color: #01d277;
|
||||||
|
}
|
||||||
|
&[data-theme="dark"] .icon-inner {
|
||||||
|
background: linear-gradient(135deg, #1a1a1a, #0a0a0a);
|
||||||
|
border-color: #01d277;
|
||||||
|
}
|
||||||
|
&[data-theme="ocean"] .icon-inner {
|
||||||
|
background: linear-gradient(135deg, #0f2027, #2c5364);
|
||||||
|
border-color: #00d4ff;
|
||||||
|
}
|
||||||
|
&[data-theme="nordic"] .icon-inner {
|
||||||
|
background: linear-gradient(135deg, #f5f0e8, #d8cdb9);
|
||||||
|
border-color: #3d6e4e;
|
||||||
|
}
|
||||||
|
&[data-theme="halloween"] .icon-inner {
|
||||||
|
background: linear-gradient(135deg, #1a0e2e, #2d1b3d);
|
||||||
|
border-color: #ff6600;
|
||||||
|
}
|
||||||
|
&[data-theme="auto"] .icon-inner {
|
||||||
|
background: conic-gradient(#f8f8f8 0deg 180deg, #1a1a1a 180deg);
|
||||||
|
border-color: #01d277;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-theme="light"] .icon-inner {
|
.theme-info {
|
||||||
background: linear-gradient(135deg, #f8f8f8 0%, #e8e8e8 100%);
|
flex: 1;
|
||||||
border: 3px solid #01d277;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-theme="dark"] .icon-inner {
|
span {
|
||||||
background: linear-gradient(135deg, #1a1a1a 0%, #0a0a0a 100%);
|
font-size: 0.85rem;
|
||||||
border: 3px solid #01d277;
|
color: $text-color-70;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
font-weight: 500;
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&[data-theme="ocean"] .icon-inner {
|
h3 {
|
||||||
background: linear-gradient(135deg, #0f2027 0%, #2c5364 100%);
|
margin: 0;
|
||||||
border: 3px solid #00d4ff;
|
font-size: 1.75rem;
|
||||||
}
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
&[data-theme="nordic"] .icon-inner {
|
@include mobile-only {
|
||||||
background: linear-gradient(135deg, #f5f0e8 0%, #d8cdb9 100%);
|
font-size: 1.4rem;
|
||||||
border: 3px solid #3d6e4e;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
&[data-theme="halloween"] .icon-inner {
|
|
||||||
background: linear-gradient(135deg, #1a0e2e 0%, #2d1b3d 100%);
|
|
||||||
border: 3px solid #ff6600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&[data-theme="auto"] .icon-inner {
|
|
||||||
background: conic-gradient(
|
|
||||||
from 0deg,
|
|
||||||
#f8f8f8 0deg 180deg,
|
|
||||||
#1a1a1a 180deg 360deg
|
|
||||||
);
|
|
||||||
border: 3px solid #01d277;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-info {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-label {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: $text-color-70;
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
font-weight: 500;
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-name {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -218,251 +173,182 @@
|
|||||||
grid-template-columns: repeat(2, 1fr);
|
grid-template-columns: repeat(2, 1fr);
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.theme-card {
|
.theme-card {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
background-color: var(--background-ui);
|
background-color: var(--background-ui);
|
||||||
border-radius: 0.75rem;
|
border-radius: 0.75rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
border: 2px solid transparent;
|
border: 2px solid transparent;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
|
||||||
@include mobile-only {
|
@include mobile-only {
|
||||||
padding: 0.85rem;
|
padding: 0.85rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
&:hover {
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
transform: scale(0.97);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: var(--highlight-color);
|
|
||||||
transform: translateY(-2px);
|
|
||||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
|
||||||
}
|
|
||||||
|
|
||||||
&--active {
|
|
||||||
border-color: var(--highlight-color);
|
|
||||||
background-color: var(--background-40);
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: none;
|
border-color: var(--highlight-color);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active {
|
&.active {
|
||||||
transform: scale(0.97);
|
border-color: var(--highlight-color);
|
||||||
}
|
background-color: var(--background-40);
|
||||||
}
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
|
||||||
&__preview {
|
|
||||||
width: 100%;
|
|
||||||
height: 120px;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
position: relative;
|
|
||||||
border: 1px solid var(--background-40);
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
height: 100px;
|
|
||||||
margin-bottom: 0.6rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview-circle {
|
&__preview {
|
||||||
|
width: 100%;
|
||||||
|
height: 120px;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
overflow: hidden;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
position: relative;
|
||||||
|
border: 1px solid var(--background-40);
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
height: 100px;
|
||||||
|
margin-bottom: 0.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-circle {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 8px;
|
||||||
|
left: 8px;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
right: 8px;
|
||||||
|
height: 20px;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview[data-theme="light"] {
|
||||||
|
background: #f8f8f8;
|
||||||
|
.preview-circle {
|
||||||
|
background: #01d277;
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
background: #fff;
|
||||||
|
border-color: rgba(8, 28, 36, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview[data-theme="dark"] {
|
||||||
|
background: #111;
|
||||||
|
.preview-circle {
|
||||||
|
background: #01d277;
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
background: #060708;
|
||||||
|
border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview[data-theme="ocean"] {
|
||||||
|
background: #0f2027;
|
||||||
|
.preview-circle {
|
||||||
|
background: #00d4ff;
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
background: #203a43;
|
||||||
|
border-color: rgba(0, 212, 255, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview[data-theme="nordic"] {
|
||||||
|
background: #f5f0e8;
|
||||||
|
.preview-circle {
|
||||||
|
background: #3d6e4e;
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
background: #fffef9;
|
||||||
|
border-color: rgba(61, 110, 78, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview[data-theme="halloween"] {
|
||||||
|
background: #1a0e2e;
|
||||||
|
.preview-circle {
|
||||||
|
background: #ff6600;
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
background: #2d1b3d;
|
||||||
|
border-color: rgba(255, 102, 0, 0.2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__preview[data-theme="auto"] {
|
||||||
|
border-color: black;
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
#f8f8f8 0%,
|
||||||
|
#f8f8f8 50%,
|
||||||
|
#111 50%,
|
||||||
|
#111 100%
|
||||||
|
);
|
||||||
|
.preview-circle {
|
||||||
|
left: auto;
|
||||||
|
right: 8px;
|
||||||
|
background: #01d277;
|
||||||
|
}
|
||||||
|
&::before {
|
||||||
|
right: auto;
|
||||||
|
width: calc(50% - 10px);
|
||||||
|
background: #fff;
|
||||||
|
border-color: rgba(8, 28, 36, 0.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__name {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--text-color);
|
||||||
|
@include mobile-only {
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
border-radius: 50%;
|
top: 0.5rem;
|
||||||
}
|
right: 0.5rem;
|
||||||
}
|
padding: 0.25rem 0.5rem;
|
||||||
|
background-color: var(--highlight-color);
|
||||||
|
color: white;
|
||||||
|
border-radius: 1rem;
|
||||||
|
font-size: 0.65rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
|
||||||
// Light Theme Preview
|
@include mobile-only {
|
||||||
&__preview[data-theme="light"] {
|
top: 0.4rem;
|
||||||
background: #f8f8f8;
|
right: 0.4rem;
|
||||||
|
padding: 0.2rem 0.4rem;
|
||||||
.preview-circle {
|
font-size: 0.6rem;
|
||||||
bottom: 8px;
|
}
|
||||||
left: 8px;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
background: #01d277;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
|
||||||
right: 8px;
|
|
||||||
height: 20px;
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid rgba(8, 28, 36, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dark Theme Preview
|
|
||||||
&__preview[data-theme="dark"] {
|
|
||||||
background: #111111;
|
|
||||||
|
|
||||||
.preview-circle {
|
|
||||||
bottom: 8px;
|
|
||||||
left: 8px;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
background: #01d277;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
|
||||||
right: 8px;
|
|
||||||
height: 20px;
|
|
||||||
background: rgba(6, 7, 8, 1);
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ocean Theme Preview
|
|
||||||
&__preview[data-theme="ocean"] {
|
|
||||||
background: #0f2027;
|
|
||||||
|
|
||||||
.preview-circle {
|
|
||||||
bottom: 8px;
|
|
||||||
left: 8px;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
background: #00d4ff;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
|
||||||
right: 8px;
|
|
||||||
height: 20px;
|
|
||||||
background: #203a43;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid rgba(0, 212, 255, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Nordic Theme Preview
|
|
||||||
&__preview[data-theme="nordic"] {
|
|
||||||
background: #f5f0e8;
|
|
||||||
|
|
||||||
.preview-circle {
|
|
||||||
bottom: 8px;
|
|
||||||
left: 8px;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
background: #3d6e4e;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
|
||||||
right: 8px;
|
|
||||||
height: 20px;
|
|
||||||
background: #fffef9;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid rgba(61, 110, 78, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Halloween Theme Preview
|
|
||||||
&__preview[data-theme="halloween"] {
|
|
||||||
background: #1a0e2e;
|
|
||||||
|
|
||||||
.preview-circle {
|
|
||||||
bottom: 8px;
|
|
||||||
left: 8px;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
background: #ff6600;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
|
||||||
right: 8px;
|
|
||||||
height: 20px;
|
|
||||||
background: #2d1b3d;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid rgba(255, 102, 0, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto Theme Preview (split)
|
|
||||||
&__preview[data-theme="auto"] {
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
#f8f8f8 0%,
|
|
||||||
#f8f8f8 50%,
|
|
||||||
#111111 50%,
|
|
||||||
#111111 100%
|
|
||||||
);
|
|
||||||
|
|
||||||
.preview-circle {
|
|
||||||
bottom: 8px;
|
|
||||||
right: 8px;
|
|
||||||
width: 30px;
|
|
||||||
height: 30px;
|
|
||||||
background: #01d277;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 8px;
|
|
||||||
left: 8px;
|
|
||||||
width: calc(50% - 10px);
|
|
||||||
height: 20px;
|
|
||||||
background: #ffffff;
|
|
||||||
border-radius: 4px;
|
|
||||||
border: 1px solid rgba(8, 28, 36, 0.1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__name {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1;
|
|
||||||
color: var(--text-color);
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__badge {
|
|
||||||
position: absolute;
|
|
||||||
top: 0.5rem;
|
|
||||||
right: 0.5rem;
|
|
||||||
padding: 0.25rem 0.5rem;
|
|
||||||
background-color: var(--highlight-color);
|
|
||||||
color: $white;
|
|
||||||
border-radius: 1rem;
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 600;
|
|
||||||
text-transform: uppercase;
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
top: 0.4rem;
|
|
||||||
right: 0.4rem;
|
|
||||||
padding: 0.2rem 0.4rem;
|
|
||||||
font-size: 0.6rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,58 +2,30 @@
|
|||||||
<section class="settings">
|
<section class="settings">
|
||||||
<div class="settings__container">
|
<div class="settings__container">
|
||||||
<!-- Profile Hero Card -->
|
<!-- Profile Hero Card -->
|
||||||
<div class="profile-hero">
|
<ProfileHero />
|
||||||
<div class="profile-hero__main">
|
|
||||||
<div class="profile-hero__avatar">
|
|
||||||
<div class="avatar-large">{{ userInitials }}</div>
|
|
||||||
</div>
|
|
||||||
<div class="profile-hero__info">
|
|
||||||
<h1 class="profile-hero__name">{{ username }}</h1>
|
|
||||||
<span :class="['profile-hero__badge', `badge--${userRole}`]">
|
|
||||||
<a v-if="userRole === 'admin'" href="/admin">{{ userRole }}</a>
|
|
||||||
<span v-else>{{ userRole }}</span>
|
|
||||||
</span>
|
|
||||||
<p class="profile-hero__member">Member since {{ memberSince }}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="profile-hero__stats">
|
|
||||||
<div class="stat-large">
|
|
||||||
<span class="stat-large__value">{{ stats.totalRequests }}</span>
|
|
||||||
<span class="stat-large__label">Requests</span>
|
|
||||||
</div>
|
|
||||||
<div class="stat-divider"></div>
|
|
||||||
<div class="stat-large">
|
|
||||||
<span class="stat-large__value">{{ stats.magnetsAdded }}</span>
|
|
||||||
<span class="stat-large__label">Magnets Added</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Settings Grid -->
|
<!-- Settings Grid -->
|
||||||
<div class="settings__grid">
|
<div class="settings__grid">
|
||||||
<!-- Left Column: Quick Settings -->
|
<!-- Left Column -->
|
||||||
<div class="settings__column settings__column--left">
|
<div class="settings__column">
|
||||||
<section class="settings-section settings-section--compact">
|
<section class="settings-section settings-section--compact">
|
||||||
<h2 class="section-header">Appearance</h2>
|
<div class="settings-section-header"><h2>Appearance</h2></div>
|
||||||
<theme-preferences />
|
<theme-preferences />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="settings-section settings-section--compact">
|
<section class="settings-section settings-section--compact">
|
||||||
<h2 class="section-header">Security</h2>
|
<security-settings />
|
||||||
<change-password />
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right Column: Data-Heavy Sections -->
|
<!-- Right Column -->
|
||||||
<div class="settings__column settings__column--right">
|
<div class="settings__column">
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h2 class="section-header">Integrations</h2>
|
<div class="settings-section-header"><h2>Integrations</h2></div>
|
||||||
<plex-settings @reload="reloadSettings" />
|
<plex-settings @reload="reloadSettings" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="settings-section">
|
<section class="settings-section">
|
||||||
<h2 class="section-header">Data & Privacy</h2>
|
|
||||||
<data-export />
|
<data-export />
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -63,12 +35,13 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { inject, computed, onMounted } from "vue";
|
import { inject, onMounted } from "vue";
|
||||||
import { useStore } from "vuex";
|
import { useStore } from "vuex";
|
||||||
import { useRoute } from "vue-router";
|
import { useRoute } from "vue-router";
|
||||||
|
import ProfileHero from "@/components/settings/ProfileHero.vue";
|
||||||
import ThemePreferences from "@/components/settings/ThemePreferences.vue";
|
import ThemePreferences from "@/components/settings/ThemePreferences.vue";
|
||||||
import PlexSettings from "@/components/settings/PlexSettings.vue";
|
import PlexSettings from "@/components/settings/PlexSettings.vue";
|
||||||
import ChangePassword from "@/components/profile/ChangePassword.vue";
|
import SecuritySettings from "@/components/settings/SecuritySettings.vue";
|
||||||
import DataExport from "@/components/settings/DataExport.vue";
|
import DataExport from "@/components/settings/DataExport.vue";
|
||||||
import { getSettings } from "../api";
|
import { getSettings } from "../api";
|
||||||
|
|
||||||
@@ -78,29 +51,6 @@
|
|||||||
error;
|
error;
|
||||||
} = inject("notifications");
|
} = inject("notifications");
|
||||||
|
|
||||||
const username = computed(() => store.getters["user/username"] || "User");
|
|
||||||
const userRole = computed(() =>
|
|
||||||
store.getters["user/admin"] ? "admin" : "user"
|
|
||||||
);
|
|
||||||
|
|
||||||
const userInitials = computed(() => {
|
|
||||||
return username.value.slice(0, 2).toUpperCase();
|
|
||||||
});
|
|
||||||
|
|
||||||
const memberSince = computed(() => {
|
|
||||||
const date = new Date();
|
|
||||||
date.setMonth(date.getMonth() - 6);
|
|
||||||
return date.toLocaleDateString("en-US", {
|
|
||||||
month: "short",
|
|
||||||
year: "numeric"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const stats = {
|
|
||||||
totalRequests: 45,
|
|
||||||
magnetsAdded: 127
|
|
||||||
};
|
|
||||||
|
|
||||||
function displayWarningIfMissingPlexAccount() {
|
function displayWarningIfMissingPlexAccount() {
|
||||||
if (route.query?.missingPlexAccount === "true") {
|
if (route.query?.missingPlexAccount === "true") {
|
||||||
notifications.error({
|
notifications.error({
|
||||||
@@ -127,13 +77,14 @@
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@import "scss/variables";
|
@import "scss/variables";
|
||||||
@import "scss/media-queries";
|
@import "scss/media-queries";
|
||||||
|
@import "scss/shared-settings";
|
||||||
|
|
||||||
.settings {
|
.settings {
|
||||||
min-height: calc(100vh - var(--header-size));
|
min-height: calc(100vh - var(--header-size));
|
||||||
padding: 2rem 1.5rem;
|
padding: 2rem 1.5rem;
|
||||||
|
|
||||||
@include mobile-only {
|
@include mobile-only {
|
||||||
padding: 1rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&__container {
|
&__container {
|
||||||
@@ -167,184 +118,6 @@
|
|||||||
@include mobile-only {
|
@include mobile-only {
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--left {
|
|
||||||
// Quick settings - lighter, more concise
|
|
||||||
}
|
|
||||||
|
|
||||||
&--right {
|
|
||||||
// Data-heavy sections
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.profile-hero {
|
|
||||||
background-color: var(--background-color-secondary);
|
|
||||||
border-radius: 0.75rem;
|
|
||||||
padding: 1.5rem;
|
|
||||||
border: 1px solid var(--background-40);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 2rem;
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
flex-direction: column;
|
|
||||||
padding: 1.5rem 1.25rem;
|
|
||||||
border-radius: 0.5rem;
|
|
||||||
text-align: center;
|
|
||||||
gap: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__main {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1.5rem;
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__avatar {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
&__info {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.35rem;
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__name {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.75rem;
|
|
||||||
font-weight: 600;
|
|
||||||
line-height: 1.1;
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.25rem 0.7rem;
|
|
||||||
border-radius: 2rem;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 600;
|
|
||||||
width: fit-content;
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
padding: 0.2rem 0.6rem;
|
|
||||||
font-size: 0.7rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.badge--admin {
|
|
||||||
background-color: var(--color-warning);
|
|
||||||
color: $black;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.badge--user {
|
|
||||||
background-color: var(--background-40);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__member {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: $text-color-70;
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__stats {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 1.75rem;
|
|
||||||
padding-left: 1.75rem;
|
|
||||||
border-left: 1px solid var(--background-40);
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
width: 100%;
|
|
||||||
padding: 1rem 0 0 0;
|
|
||||||
border-left: none;
|
|
||||||
border-top: 1px solid var(--background-40);
|
|
||||||
justify-content: center;
|
|
||||||
gap: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.avatar-large {
|
|
||||||
width: 70px;
|
|
||||||
height: 70px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
var(--highlight-color),
|
|
||||||
var(--color-green-70)
|
|
||||||
);
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
font-size: 1.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: $white;
|
|
||||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
font-size: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-large {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.25rem;
|
|
||||||
|
|
||||||
&__value {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: var(--highlight-color);
|
|
||||||
line-height: 1;
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
font-size: 1.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&__label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: $text-color-70;
|
|
||||||
text-transform: uppercase;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.5px;
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.stat-divider {
|
|
||||||
width: 1px;
|
|
||||||
height: 45px;
|
|
||||||
background-color: var(--background-40);
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
height: 45px;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -355,36 +128,16 @@
|
|||||||
border: 1px solid var(--background-40);
|
border: 1px solid var(--background-40);
|
||||||
|
|
||||||
@include mobile-only {
|
@include mobile-only {
|
||||||
padding: 1rem;
|
padding: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
&--compact {
|
&--compact {
|
||||||
// Tighter spacing for quick settings
|
// Tighter padding for quick settings, but same header size
|
||||||
.section-header {
|
padding: 1rem;
|
||||||
font-size: 1.25rem;
|
|
||||||
margin-bottom: 0.85rem;
|
|
||||||
padding-bottom: 0.65rem;
|
|
||||||
|
|
||||||
@include mobile-only {
|
@include mobile-only {
|
||||||
font-size: 1.2rem;
|
padding: 0.5rem;
|
||||||
margin-bottom: 0.75rem;
|
|
||||||
padding-bottom: 0.6rem;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-header {
|
|
||||||
margin: 0 0 1rem 0;
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 600;
|
|
||||||
padding-bottom: 0.75rem;
|
|
||||||
border-bottom: 1px solid var(--background-40);
|
|
||||||
|
|
||||||
@include mobile-only {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
margin-bottom: 0.85rem;
|
|
||||||
padding-bottom: 0.65rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
30
src/scss/shared-settings.scss
Normal file
30
src/scss/shared-settings.scss
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
@import "./media-queries.scss";
|
||||||
|
|
||||||
|
.settings-section-card {
|
||||||
|
padding: 0.85rem;
|
||||||
|
background-color: var(--background-ui);
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
border-left: 3px solid var(--highlight-color);
|
||||||
|
|
||||||
|
@include mobile-only {
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.settings-section-header {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-color-70);
|
||||||
|
font-size: 0.95rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user