mirror of
https://github.com/KevinMidboe/seasoned.git
synced 2026-03-11 03:49:07 +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>
|
||||
<div class="change-password">
|
||||
<div class="password-card">
|
||||
<p class="password-info">
|
||||
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">
|
||||
<form class="password-form" @submit.prevent>
|
||||
<seasoned-input
|
||||
v-model="oldPassword"
|
||||
placeholder="Current password"
|
||||
@@ -72,49 +67,8 @@
|
||||
newPasswordRepeat.value = password;
|
||||
}
|
||||
|
||||
function addWarningMessage(message: string, title?: string) {
|
||||
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() {
|
||||
async function changePassword(event: CustomEvent) {
|
||||
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({
|
||||
message: "Password change is currently disabled",
|
||||
title: "Feature Disabled",
|
||||
@@ -153,20 +107,6 @@
|
||||
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 {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -36,7 +36,10 @@
|
||||
|
||||
<div class="generator-options">
|
||||
<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
|
||||
v-model.number="wordCount"
|
||||
type="range"
|
||||
@@ -45,7 +48,10 @@
|
||||
class="slider"
|
||||
@input="generateWordsPassword"
|
||||
/>
|
||||
<span class="option-value">{{ wordCount }}</span>
|
||||
<div class="slider-labels">
|
||||
<span>3</span>
|
||||
<span>7</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,7 +76,10 @@
|
||||
|
||||
<div class="generator-options">
|
||||
<div class="option-row">
|
||||
<label>Length: {{ charLength }}</label>
|
||||
<div class="slider-header">
|
||||
<label>Length</label>
|
||||
<span class="slider-value">{{ charLength }}</span>
|
||||
</div>
|
||||
<input
|
||||
v-model.number="charLength"
|
||||
type="range"
|
||||
@@ -79,7 +88,10 @@
|
||||
class="slider"
|
||||
@input="generateCharsPassword"
|
||||
/>
|
||||
<span class="option-value">{{ charLength }}</span>
|
||||
<div class="slider-labels">
|
||||
<span>12</span>
|
||||
<span>46</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="option-row checkbox-row">
|
||||
@@ -133,7 +145,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from "vue";
|
||||
import { ref, watch, onMounted } from "vue";
|
||||
import IconActivity from "@/icons/IconActivity.vue";
|
||||
import { useRandomWords } from "@/composables/useRandomWords";
|
||||
|
||||
@@ -360,12 +372,12 @@
|
||||
.option-row {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
gap: 0.75rem;
|
||||
|
||||
label {
|
||||
font-size: 0.85rem;
|
||||
font-size: 0.95rem;
|
||||
color: $text-color;
|
||||
font-weight: 500;
|
||||
font-weight: 600;
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 6px;
|
||||
border-radius: 3px;
|
||||
height: 10px;
|
||||
border-radius: 5px;
|
||||
background: var(--background-40);
|
||||
outline: 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 {
|
||||
appearance: none;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
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 {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
background: var(--highlight-color);
|
||||
cursor: pointer;
|
||||
cursor: grab;
|
||||
border: none;
|
||||
}
|
||||
}
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);
|
||||
transition: all 0.2s;
|
||||
|
||||
.option-value {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--highlight-color);
|
||||
text-align: center;
|
||||
@include mobile-only {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
&: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 {
|
||||
|
||||
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
|
||||
v-for="theme in themes"
|
||||
:key="theme.value"
|
||||
:class="[
|
||||
'theme-card',
|
||||
{ 'theme-card--active': selectedTheme === theme.value }
|
||||
]"
|
||||
:class="['theme-card', { active: selectedTheme === theme.value }]"
|
||||
@click="selectTheme(theme.value)"
|
||||
>
|
||||
<div class="theme-card__preview" :data-theme="theme.value">
|
||||
@@ -35,65 +32,31 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from "vue";
|
||||
import { computed, onMounted } from "vue";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
|
||||
interface Theme {
|
||||
value: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const themes: Theme[] = [
|
||||
const themes = [
|
||||
{ value: "auto", label: "Auto" },
|
||||
{ value: "light", label: "Light" },
|
||||
{ value: "dark", label: "Dark" },
|
||||
{ value: "ocean", label: "Ocean" },
|
||||
{ value: "nordic", label: "Nordic" },
|
||||
{ value: "halloween", label: "Halloween" }
|
||||
];
|
||||
] as const;
|
||||
|
||||
const selectedTheme = ref("auto");
|
||||
const { currentTheme, savedTheme, setTheme } = useTheme();
|
||||
const selectedTheme = currentTheme;
|
||||
|
||||
const currentThemeName = computed(() => {
|
||||
const theme = themes.find(t => t.value === selectedTheme.value);
|
||||
return theme ? theme.label : "Auto";
|
||||
});
|
||||
|
||||
function systemDarkModeEnabled() {
|
||||
const computedStyle = window.getComputedStyle(document.body);
|
||||
if (computedStyle?.colorScheme != null) {
|
||||
return computedStyle.colorScheme.includes("dark");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
const currentThemeName = computed(
|
||||
() => themes.find(t => t.value === selectedTheme.value)?.label ?? "Auto"
|
||||
);
|
||||
|
||||
function selectTheme(theme: string) {
|
||||
selectedTheme.value = theme;
|
||||
|
||||
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);
|
||||
setTheme(theme as any);
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Load saved preference or default to auto
|
||||
const savedTheme = localStorage.getItem("theme-preference") || "auto";
|
||||
selectTheme(savedTheme);
|
||||
selectedTheme.value = savedTheme.value;
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -113,99 +76,91 @@
|
||||
padding: 1.5rem;
|
||||
border-radius: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
.theme-display {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1.5rem;
|
||||
|
||||
@include mobile-only {
|
||||
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;
|
||||
@include mobile-only {
|
||||
gap: 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.icon-inner {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
.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 {
|
||||
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 {
|
||||
background: linear-gradient(135deg, #f8f8f8 0%, #e8e8e8 100%);
|
||||
border: 3px solid #01d277;
|
||||
.theme-info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
&[data-theme="dark"] .icon-inner {
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #0a0a0a 100%);
|
||||
border: 3px solid #01d277;
|
||||
span {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
&[data-theme="ocean"] .icon-inner {
|
||||
background: linear-gradient(135deg, #0f2027 0%, #2c5364 100%);
|
||||
border: 3px solid #00d4ff;
|
||||
}
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
|
||||
&[data-theme="nordic"] .icon-inner {
|
||||
background: linear-gradient(135deg, #f5f0e8 0%, #d8cdb9 100%);
|
||||
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;
|
||||
@include mobile-only {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,251 +173,182 @@
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.theme-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 1rem;
|
||||
background-color: var(--background-ui);
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 2px solid transparent;
|
||||
text-align: center;
|
||||
.theme-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
padding: 1rem;
|
||||
background-color: var(--background-ui);
|
||||
border-radius: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
border: 2px solid transparent;
|
||||
text-align: center;
|
||||
|
||||
@include mobile-only {
|
||||
padding: 0.85rem;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
@include mobile-only {
|
||||
padding: 0.85rem;
|
||||
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 {
|
||||
transform: none;
|
||||
border-color: var(--highlight-color);
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: scale(0.97);
|
||||
}
|
||||
}
|
||||
|
||||
&__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;
|
||||
&.active {
|
||||
border-color: var(--highlight-color);
|
||||
background-color: var(--background-40);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.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;
|
||||
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
|
||||
&__preview[data-theme="light"] {
|
||||
background: #f8f8f8;
|
||||
|
||||
.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: #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;
|
||||
@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">
|
||||
<div class="settings__container">
|
||||
<!-- Profile Hero Card -->
|
||||
<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>
|
||||
<ProfileHero />
|
||||
|
||||
<!-- Settings Grid -->
|
||||
<div class="settings__grid">
|
||||
<!-- Left Column: Quick Settings -->
|
||||
<div class="settings__column settings__column--left">
|
||||
<!-- Left Column -->
|
||||
<div class="settings__column">
|
||||
<section class="settings-section settings-section--compact">
|
||||
<h2 class="section-header">Appearance</h2>
|
||||
<div class="settings-section-header"><h2>Appearance</h2></div>
|
||||
<theme-preferences />
|
||||
</section>
|
||||
|
||||
<section class="settings-section settings-section--compact">
|
||||
<h2 class="section-header">Security</h2>
|
||||
<change-password />
|
||||
<security-settings />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<!-- Right Column: Data-Heavy Sections -->
|
||||
<div class="settings__column settings__column--right">
|
||||
<!-- Right Column -->
|
||||
<div class="settings__column">
|
||||
<section class="settings-section">
|
||||
<h2 class="section-header">Integrations</h2>
|
||||
<div class="settings-section-header"><h2>Integrations</h2></div>
|
||||
<plex-settings @reload="reloadSettings" />
|
||||
</section>
|
||||
|
||||
<section class="settings-section">
|
||||
<h2 class="section-header">Data & Privacy</h2>
|
||||
<data-export />
|
||||
</section>
|
||||
</div>
|
||||
@@ -63,12 +35,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, computed, onMounted } from "vue";
|
||||
import { inject, onMounted } from "vue";
|
||||
import { useStore } from "vuex";
|
||||
import { useRoute } from "vue-router";
|
||||
import ProfileHero from "@/components/settings/ProfileHero.vue";
|
||||
import ThemePreferences from "@/components/settings/ThemePreferences.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 { getSettings } from "../api";
|
||||
|
||||
@@ -78,29 +51,6 @@
|
||||
error;
|
||||
} = 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() {
|
||||
if (route.query?.missingPlexAccount === "true") {
|
||||
notifications.error({
|
||||
@@ -127,13 +77,14 @@
|
||||
<style lang="scss" scoped>
|
||||
@import "scss/variables";
|
||||
@import "scss/media-queries";
|
||||
@import "scss/shared-settings";
|
||||
|
||||
.settings {
|
||||
min-height: calc(100vh - var(--header-size));
|
||||
padding: 2rem 1.5rem;
|
||||
|
||||
@include mobile-only {
|
||||
padding: 1rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
&__container {
|
||||
@@ -167,184 +118,6 @@
|
||||
@include mobile-only {
|
||||
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);
|
||||
|
||||
@include mobile-only {
|
||||
padding: 1rem;
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
&--compact {
|
||||
// Tighter spacing for quick settings
|
||||
.section-header {
|
||||
font-size: 1.25rem;
|
||||
margin-bottom: 0.85rem;
|
||||
padding-bottom: 0.65rem;
|
||||
// Tighter padding for quick settings, but same header size
|
||||
padding: 1rem;
|
||||
|
||||
@include mobile-only {
|
||||
font-size: 1.2rem;
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.6rem;
|
||||
}
|
||||
@include mobile-only {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
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