refactor: migrate settings page styles from VE to SCSS + BEM

This commit is contained in:
Hachi-R 2025-01-21 14:39:15 -03:00
parent eed28d7444
commit c51b61501e
14 changed files with 269 additions and 108 deletions

View File

@ -0,0 +1,27 @@
@use "../../scss/globals.scss";
.add-download-source-modal {
&__container {
display: flex;
flex-direction: column;
gap: globals.$spacing-unit;
min-width: 500px;
}
&__validation-result {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: calc(globals.$spacing-unit * 3);
}
&__validation-info {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit / 2);
}
&__validate-button {
align-self: flex-end;
}
}

View File

@ -2,7 +2,6 @@ import { useCallback, useContext, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button, Modal, TextField } from "@renderer/components"; import { Button, Modal, TextField } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css";
import { settingsContext } from "@renderer/context"; import { settingsContext } from "@renderer/context";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
@ -11,6 +10,7 @@ import { yupResolver } from "@hookform/resolvers/yup";
import { downloadSourcesTable } from "@renderer/dexie"; import { downloadSourcesTable } from "@renderer/dexie";
import type { DownloadSourceValidationResult } from "@types"; import type { DownloadSourceValidationResult } from "@types";
import { downloadSourcesWorker } from "@renderer/workers"; import { downloadSourcesWorker } from "@renderer/workers";
import "./add-download-source-modal.scss";
interface AddDownloadSourceModalProps { interface AddDownloadSourceModalProps {
visible: boolean; visible: boolean;
@ -138,14 +138,7 @@ export function AddDownloadSourceModal({
description={t("add_download_source_description")} description={t("add_download_source_description")}
onClose={onClose} onClose={onClose}
> >
<div <div className="add-download-source-modal__container">
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
minWidth: "500px",
}}
>
<TextField <TextField
{...register("url")} {...register("url")}
label={t("download_source_url")} label={t("download_source_url")}
@ -155,7 +148,7 @@ export function AddDownloadSourceModal({
<Button <Button
type="button" type="button"
theme="outline" theme="outline"
style={{ alignSelf: "flex-end" }} className="add-download-source-modal__validate-button"
onClick={handleSubmit(onSubmit)} onClick={handleSubmit(onSubmit)}
disabled={isSubmitting || isLoading} disabled={isSubmitting || isLoading}
> >
@ -165,21 +158,8 @@ export function AddDownloadSourceModal({
/> />
{validationResult && ( {validationResult && (
<div <div className="add-download-source-modal__validation-result">
style={{ <div className="add-download-source-modal__validation-info">
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: `${SPACING_UNIT * 3}px`,
}}
>
<div
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT / 2}px`,
}}
>
<h4>{validationResult?.name}</h4> <h4>{validationResult?.name}</h4>
<small> <small>
{t("found_download_option", { {t("found_download_option", {

View File

@ -0,0 +1,67 @@
@use "../../scss/globals.scss";
.settings-account {
&__form {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
}
&__section {
display: flex;
flex-direction: column;
gap: globals.$spacing-unit;
}
&__actions {
display: flex;
gap: globals.$spacing-unit;
margin-top: globals.$spacing-unit;
}
&__subscription-info {
display: flex;
flex-direction: column;
gap: calc(globals.$spacing-unit * 2);
}
&__subscription-button {
place-self: flex-start;
}
&__blocked-users {
list-style: none;
margin: 0;
padding: 0;
display: flex;
flex-direction: column;
gap: globals.$spacing-unit;
}
&__blocked-user {
display: flex;
align-items: center;
justify-content: space-between;
gap: globals.$spacing-unit;
}
&__user-info {
display: flex;
gap: globals.$spacing-unit;
align-items: center;
}
&__user-avatar {
filter: grayscale(100%);
}
&__unblock-button {
color: globals.$danger-color;
cursor: pointer;
background: none;
border: none;
padding: 0;
display: flex;
align-items: center;
}
}

View File

@ -1,9 +1,6 @@
import { Avatar, Button, SelectField } from "@renderer/components"; import { Avatar, Button, SelectField } from "@renderer/components";
import { SPACING_UNIT } from "@renderer/theme.css";
import { Controller, useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as styles from "./settings-account.css";
import { useDate, useToast, useUserDetails } from "@renderer/hooks"; import { useDate, useToast, useUserDetails } from "@renderer/hooks";
import { useCallback, useContext, useEffect, useState } from "react"; import { useCallback, useContext, useEffect, useState } from "react";
import { import {
@ -14,6 +11,7 @@ import {
} from "@primer/octicons-react"; } from "@primer/octicons-react";
import { settingsContext } from "@renderer/context"; import { settingsContext } from "@renderer/context";
import { AuthPage } from "@shared"; import { AuthPage } from "@shared";
import "./settings-account.scss";
interface FormValues { interface FormValues {
profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE"; profileVisibility: "PUBLIC" | "FRIENDS" | "PRIVATE";
@ -145,7 +143,7 @@ export function SettingsAccount() {
if (!userDetails) return null; if (!userDetails) return null;
return ( return (
<form className={styles.form} onSubmit={handleSubmit(onSubmit)}> <form className="settings-account__form" onSubmit={handleSubmit(onSubmit)}>
<Controller <Controller
control={control} control={control}
name="profileVisibility" name="profileVisibility"
@ -158,7 +156,7 @@ export function SettingsAccount() {
}; };
return ( return (
<section> <section className="settings-account__section">
<SelectField <SelectField
label={t("profile_visibility")} label={t("profile_visibility")}
value={field.value} value={field.value}
@ -177,19 +175,11 @@ export function SettingsAccount() {
}} }}
/> />
<section> <section className="settings-account__section">
<h4>{t("current_email")}</h4> <h4>{t("current_email")}</h4>
<p>{userDetails?.email ?? t("no_email_account")}</p> <p>{userDetails?.email ?? t("no_email_account")}</p>
<div <div className="settings-account__actions">
style={{
display: "flex",
justifyContent: "start",
alignItems: "center",
gap: `${SPACING_UNIT}px`,
marginTop: `${SPACING_UNIT * 2}px`,
}}
>
<Button <Button
theme="outline" theme="outline"
onClick={() => window.electron.openAuthWindow(AuthPage.UpdateEmail)} onClick={() => window.electron.openAuthWindow(AuthPage.UpdateEmail)}
@ -210,28 +200,14 @@ export function SettingsAccount() {
</div> </div>
</section> </section>
<section <section className="settings-account__section">
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
}}
>
<h3>Hydra Cloud</h3> <h3>Hydra Cloud</h3>
<div <div className="settings-account__subscription-info">
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT}px`,
}}
>
{getHydraCloudSectionContent().description} {getHydraCloudSectionContent().description}
</div> </div>
<Button <Button
style={{ className="settings-account__subscription-button"
placeSelf: "flex-start",
}}
theme="outline" theme="outline"
onClick={() => window.electron.openCheckout()} onClick={() => window.electron.openCheckout()}
> >
@ -240,29 +216,17 @@ export function SettingsAccount() {
</Button> </Button>
</section> </section>
<section <section className="settings-account__section">
style={{
display: "flex",
flexDirection: "column",
gap: `${SPACING_UNIT * 2}px`,
}}
>
<h3>{t("blocked_users")}</h3> <h3>{t("blocked_users")}</h3>
{blockedUsers.length > 0 ? ( {blockedUsers.length > 0 ? (
<ul className={styles.blockedUsersList}> <ul className="settings-account__blocked-users">
{blockedUsers.map((user) => { {blockedUsers.map((user) => {
return ( return (
<li key={user.id} className={styles.blockedUser}> <li key={user.id} className="settings-account__blocked-user">
<div <div className="settings-account__user-info">
style={{
display: "flex",
gap: `${SPACING_UNIT}px`,
alignItems: "center",
}}
>
<Avatar <Avatar
style={{ filter: "grayscale(100%)" }} className="settings-account__user-avatar"
size={32} size={32}
src={user.profileImageUrl} src={user.profileImageUrl}
alt={user.displayName} alt={user.displayName}
@ -272,7 +236,7 @@ export function SettingsAccount() {
<button <button
type="button" type="button"
className={styles.unblockButton} className="settings-account__unblock-button"
onClick={() => handleUnblockClick(user.id)} onClick={() => handleUnblockClick(user.id)}
disabled={isUnblocking} disabled={isUnblocking}
> >

View File

@ -0,0 +1,13 @@
@use "../../scss/globals.scss";
.settings-behavior {
&__checkbox-container {
opacity: globals.$disabled-opacity;
cursor: not-allowed;
&--enabled {
opacity: 1;
cursor: pointer;
}
}
}

View File

@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { CheckboxField } from "@renderer/components"; import { CheckboxField } from "@renderer/components";
import { useAppSelector } from "@renderer/hooks"; import { useAppSelector } from "@renderer/hooks";
import { settingsContext } from "@renderer/context"; import { settingsContext } from "@renderer/context";
import "./settings-behavior.scss";
export function SettingsBehavior() { export function SettingsBehavior() {
const userPreferences = useAppSelector( const userPreferences = useAppSelector(
@ -77,10 +78,11 @@ export function SettingsBehavior() {
)} )}
{showRunAtStartup && ( {showRunAtStartup && (
<div style={{ opacity: form.runAtStartup ? 1 : 0.5 }}> <div
className={`settings-behavior__checkbox-container ${form.runAtStartup ? "settings-behavior__checkbox-container--enabled" : ""}`}
>
<CheckboxField <CheckboxField
label={t("launch_minimized")} label={t("launch_minimized")}
style={{ cursor: form.runAtStartup ? "pointer" : "not-allowed" }}
checked={form.runAtStartup && form.startMinimized} checked={form.runAtStartup && form.startMinimized}
disabled={!form.runAtStartup} disabled={!form.runAtStartup}
onChange={() => { onChange={() => {

View File

@ -0,0 +1,56 @@
@use "../../scss/globals.scss";
.settings-download-sources {
&__list {
padding: 0;
margin: 0;
gap: calc(globals.$spacing-unit * 2);
display: flex;
flex-direction: column;
}
&__item {
display: flex;
flex-direction: column;
background-color: globals.$dark-background-color;
border-radius: 8px;
padding: calc(globals.$spacing-unit * 2);
gap: globals.$spacing-unit;
border: solid 1px globals.$border-color;
transition: all ease 0.2s;
&--syncing {
opacity: globals.$disabled-opacity;
}
}
&__item-header {
margin-bottom: globals.$spacing-unit;
display: flex;
flex-direction: column;
gap: globals.$spacing-unit;
}
&__header {
display: flex;
justify-content: space-between;
align-items: center;
}
&__navigate-button {
display: flex;
align-items: center;
gap: globals.$spacing-unit;
color: globals.$muted-color;
text-decoration: underline;
cursor: pointer;
background: none;
border: none;
padding: 0;
&:disabled {
cursor: default;
text-decoration: none;
}
}
}

View File

@ -3,7 +3,6 @@ import { useContext, useEffect, useState } from "react";
import { TextField, Button, Badge } from "@renderer/components"; import { TextField, Button, Badge } from "@renderer/components";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import * as styles from "./settings-download-sources.css";
import type { DownloadSource } from "@types"; import type { DownloadSource } from "@types";
import { NoEntryIcon, PlusCircleIcon, SyncIcon } from "@primer/octicons-react"; import { NoEntryIcon, PlusCircleIcon, SyncIcon } from "@primer/octicons-react";
import { AddDownloadSourceModal } from "./add-download-source-modal"; import { AddDownloadSourceModal } from "./add-download-source-modal";
@ -14,6 +13,7 @@ import { downloadSourcesTable } from "@renderer/dexie";
import { downloadSourcesWorker } from "@renderer/workers"; import { downloadSourcesWorker } from "@renderer/workers";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { setFilters, clearFilters } from "@renderer/features"; import { setFilters, clearFilters } from "@renderer/features";
import "./settings-download-sources.scss";
export function SettingsDownloadSources() { export function SettingsDownloadSources() {
const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] = const [showAddDownloadSourceModal, setShowAddDownloadSourceModal] =
@ -118,7 +118,7 @@ export function SettingsDownloadSources() {
<p>{t("download_sources_description")}</p> <p>{t("download_sources_description")}</p>
<div className={styles.downloadSourcesHeader}> <div className="settings-download-sources__header">
<Button <Button
type="button" type="button"
theme="outline" theme="outline"
@ -144,15 +144,13 @@ export function SettingsDownloadSources() {
</Button> </Button>
</div> </div>
<ul className={styles.downloadSources}> <ul className="settings-download-sources__list">
{downloadSources.map((downloadSource) => ( {downloadSources.map((downloadSource) => (
<li <li
key={downloadSource.id} key={downloadSource.id}
className={styles.downloadSourceItem({ className={`settings-download-sources__item ${isSyncingDownloadSources ? "settings-download-sources__item--syncing" : ""}`}
isSyncing: isSyncingDownloadSources,
})}
> >
<div className={styles.downloadSourceItemHeader}> <div className="settings-download-sources__item-header">
<h2>{downloadSource.name}</h2> <h2>{downloadSource.name}</h2>
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
@ -161,7 +159,7 @@ export function SettingsDownloadSources() {
<button <button
type="button" type="button"
className={styles.navigateToCatalogueButton} className="settings-download-sources__navigate-button"
disabled={!downloadSource.fingerprint} disabled={!downloadSource.fingerprint}
onClick={() => navigateToCatalogue(downloadSource.fingerprint)} onClick={() => navigateToCatalogue(downloadSource.fingerprint)}
> >

View File

@ -0,0 +1,12 @@
@use "../../scss/globals.scss";
.settings-general {
display: flex;
flex-direction: column;
gap: globals.$spacing-unit * 2;
&__notifications-title {
margin-top: calc(globals.$spacing-unit * 2);
margin-bottom: globals.$spacing-unit;
}
}

View File

@ -11,6 +11,7 @@ import { changeLanguage } from "i18next";
import languageResources from "@locales"; import languageResources from "@locales";
import { orderBy } from "lodash-es"; import { orderBy } from "lodash-es";
import { settingsContext } from "@renderer/context"; import { settingsContext } from "@renderer/context";
import "./settings-general.scss";
interface LanguageOption { interface LanguageOption {
option: string; option: string;
@ -114,7 +115,7 @@ export function SettingsGeneral() {
} }
return ( return (
<> <div className="settings-general">
<TextField <TextField
label={t("downloads_path")} label={t("downloads_path")}
value={form.downloadsPath} value={form.downloadsPath}
@ -138,7 +139,9 @@ export function SettingsGeneral() {
}))} }))}
/> />
<p>{t("notifications")}</p> <p className="settings-general__notifications-title">
{t("notifications")}
</p>
<CheckboxField <CheckboxField
label={t("enable_download_notifications")} label={t("enable_download_notifications")}
@ -171,6 +174,6 @@ export function SettingsGeneral() {
}) })
} }
/> />
</> </div>
); );
} }

View File

@ -0,0 +1,18 @@
@use "../../scss/globals.scss";
.settings-real-debrid {
&__form {
display: flex;
flex-direction: column;
gap: globals.$spacing-unit;
}
&__description {
margin-bottom: calc(globals.$spacing-unit * 2);
}
&__submit-button {
align-self: flex-end;
width: 100%;
}
}

View File

@ -2,11 +2,10 @@ import { useContext, useEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import { Button, CheckboxField, Link, TextField } from "@renderer/components"; import { Button, CheckboxField, Link, TextField } from "@renderer/components";
import * as styles from "./settings-real-debrid.css"; import "./settings-real-debrid.scss";
import { useAppSelector, useToast } from "@renderer/hooks"; import { useAppSelector, useToast } from "@renderer/hooks";
import { SPACING_UNIT } from "@renderer/theme.css";
import { settingsContext } from "@renderer/context"; import { settingsContext } from "@renderer/context";
const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken"; const REAL_DEBRID_API_TOKEN_URL = "https://real-debrid.com/apitoken";
@ -78,8 +77,10 @@ export function SettingsRealDebrid() {
(form.useRealDebrid && !form.realDebridApiToken) || isLoading; (form.useRealDebrid && !form.realDebridApiToken) || isLoading;
return ( return (
<form className={styles.form} onSubmit={handleFormSubmit}> <form className="settings-real-debrid__form" onSubmit={handleFormSubmit}>
<p className={styles.description}>{t("real_debrid_description")}</p> <p className="settings-real-debrid__description">
{t("real_debrid_description")}
</p>
<CheckboxField <CheckboxField
label={t("enable_real_debrid")} label={t("enable_real_debrid")}
@ -100,8 +101,12 @@ export function SettingsRealDebrid() {
onChange={(event) => onChange={(event) =>
setForm({ ...form, realDebridApiToken: event.target.value }) setForm({ ...form, realDebridApiToken: event.target.value })
} }
rightContent={
<Button type="submit" disabled={isButtonDisabled}>
{t("save_changes")}
</Button>
}
placeholder="API Token" placeholder="API Token"
containerProps={{ style: { marginTop: `${SPACING_UNIT}px` } }}
hint={ hint={
<Trans i18nKey="real_debrid_api_token_hint" ns="settings"> <Trans i18nKey="real_debrid_api_token_hint" ns="settings">
<Link to={REAL_DEBRID_API_TOKEN_URL} /> <Link to={REAL_DEBRID_API_TOKEN_URL} />
@ -109,14 +114,6 @@ export function SettingsRealDebrid() {
} }
/> />
)} )}
<Button
type="submit"
style={{ alignSelf: "flex-end", marginTop: `${SPACING_UNIT * 2}px` }}
disabled={isButtonDisabled}
>
{t("save_changes")}
</Button>
</form> </form>
); );
} }

View File

@ -0,0 +1,27 @@
@use "../../scss/globals.scss";
.settings {
&__container {
padding: 24px;
width: 100%;
display: flex;
}
&__content {
background-color: globals.$background-color;
width: 100%;
height: 100%;
padding: calc(globals.$spacing-unit * 3);
border: solid 1px globals.$border-color;
box-shadow: 0px 0px 15px 0px #000000;
border-radius: 8px;
gap: calc(globals.$spacing-unit * 2);
display: flex;
flex-direction: column;
}
&__categories {
display: flex;
gap: globals.$spacing-unit;
}
}

View File

@ -1,11 +1,8 @@
import { Button } from "@renderer/components"; import { Button } from "@renderer/components";
import * as styles from "./settings.css";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { SettingsRealDebrid } from "./settings-real-debrid"; import { SettingsRealDebrid } from "./settings-real-debrid";
import { SettingsGeneral } from "./settings-general"; import { SettingsGeneral } from "./settings-general";
import { SettingsBehavior } from "./settings-behavior"; import { SettingsBehavior } from "./settings-behavior";
import { SettingsDownloadSources } from "./settings-download-sources"; import { SettingsDownloadSources } from "./settings-download-sources";
import { import {
SettingsContextConsumer, SettingsContextConsumer,
@ -14,10 +11,10 @@ import {
import { SettingsAccount } from "./settings-account"; import { SettingsAccount } from "./settings-account";
import { useUserDetails } from "@renderer/hooks"; import { useUserDetails } from "@renderer/hooks";
import { useMemo } from "react"; import { useMemo } from "react";
import "./settings.scss";
export default function Settings() { export default function Settings() {
const { t } = useTranslation("settings"); const { t } = useTranslation("settings");
const { userDetails } = useUserDetails(); const { userDetails } = useUserDetails();
const categories = useMemo(() => { const categories = useMemo(() => {
@ -57,9 +54,9 @@ export default function Settings() {
}; };
return ( return (
<section className={styles.container}> <section className="settings__container">
<div className={styles.content}> <div className="settings__content">
<section className={styles.settingsCategories}> <section className="settings__categories">
{categories.map((category, index) => ( {categories.map((category, index) => (
<Button <Button
key={category} key={category}