feat: add dark mode (#409)
* feat: add dark mode. Issue #348 * feat: add darkmode and fix btn for the landing page and login. Issue #348 * test: update test cases. Issue #348 * chore: fix sonarcloud code smell. Issue #348
This commit is contained in:
+25
-2
@@ -1,4 +1,4 @@
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
createBrowserRouter,
|
||||
Navigate,
|
||||
@@ -24,6 +24,9 @@ import './styles/custom.scss';
|
||||
*/
|
||||
function App(): React.ReactNode {
|
||||
const { signed, checkCurrentAuthUser } = useContext(AuthContext);
|
||||
const [theme, setTheme] = useState(() => {
|
||||
return localStorage.getItem('theme') ?? 'light';
|
||||
});
|
||||
|
||||
/**
|
||||
* Routes for the users who are not signed in.
|
||||
@@ -90,8 +93,28 @@ function App(): React.ReactNode {
|
||||
checkCurrentAuthUser(window.location.pathname);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
document.body.setAttribute('data-bs-theme', theme);
|
||||
localStorage.setItem('theme', theme);
|
||||
}, [theme]);
|
||||
|
||||
const toggleTheme = () => {
|
||||
setTheme(prev => (prev === 'light' ? 'dark' : 'light'));
|
||||
};
|
||||
|
||||
return (
|
||||
<RouterProvider router={browserRouter} />
|
||||
<>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-dark"
|
||||
onClick={toggleTheme}
|
||||
>
|
||||
{theme === 'dark' ? '☀ Light Mode' : '🌙 Dark Mode'}
|
||||
</button>
|
||||
|
||||
{/* Your routes or content here */}
|
||||
<RouterProvider router={browserRouter} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('ContentHeader test cases', () => {
|
||||
expect(getByText(headerProps.h2GreenText)).toBeDefined();
|
||||
|
||||
// renders only if it's the home (dashboard)
|
||||
expect(getByText("Add note")).toBeDefined();
|
||||
expect(getByText("Add task")).toBeDefined();
|
||||
expect(getByText("New note")).toBeDefined();
|
||||
expect(getByText("New task")).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('Account Component', () => {
|
||||
|
||||
it('should render the Account component', () => {
|
||||
const { getByText } = renderAccount();
|
||||
expect(getByText('Change your info')).toBeDefined();
|
||||
expect(getByText('Update only what you need. Blank fields will not be updated')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should change language when a language button is clicked', () => {
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 324 KiB |
@@ -5,7 +5,7 @@ import { TasksChartResponse } from '../../types/TasksChartResponse';
|
||||
import api from '../../api-service/api';
|
||||
import ApiConfig from '../../api-service/apiConfig';
|
||||
import ChartIcon from '../../assets/chart-icon.svg';
|
||||
import './style.css';
|
||||
import './style.scss';
|
||||
|
||||
type Series = {
|
||||
label: string;
|
||||
|
||||
+3
-1
@@ -1,5 +1,7 @@
|
||||
@import '../../styles/theme.scss';
|
||||
|
||||
.completed-tasks {
|
||||
background-color: #fff;
|
||||
background-color: var(--bs-body-bg);
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useContext } from 'react';
|
||||
import { Col, Row } from 'react-bootstrap';
|
||||
import { PlusCircleFill } from 'react-bootstrap-icons';
|
||||
import { NavLink } from 'react-router';
|
||||
import SidebarContext from '../../context/SidebarContext';
|
||||
|
||||
@@ -55,8 +54,7 @@ const ContentHeader: React.FC<Props> = (props: Props): React.ReactNode => {
|
||||
type="button"
|
||||
className="home-new-item w-45 mb-2"
|
||||
>
|
||||
<PlusCircleFill size={25} />
|
||||
Add task
|
||||
New task
|
||||
</button>
|
||||
</NavLink>
|
||||
<NavLink to="/notes/new" onClick={() => setNewPage('/notes/new')}>
|
||||
@@ -64,8 +62,7 @@ const ContentHeader: React.FC<Props> = (props: Props): React.ReactNode => {
|
||||
type="button"
|
||||
className="home-new-item w-45 ms-2"
|
||||
>
|
||||
<PlusCircleFill size={25} />
|
||||
Add note
|
||||
New note
|
||||
</button>
|
||||
</NavLink>
|
||||
</Col>
|
||||
|
||||
+6
-2
@@ -1,9 +1,13 @@
|
||||
/* custom-datepicker.css */
|
||||
@import '../../styles/theme.scss';
|
||||
|
||||
/* custom-datepicker.scss */
|
||||
.react-datepicker__input-container input {
|
||||
font-size: 16px; /* Prevents iOS zoom on focus */
|
||||
width: 100%;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ced4da;
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
.react-datepicker-popper {
|
||||
@@ -2,10 +2,10 @@ import React, { useEffect, useState } from 'react';
|
||||
import { Col, Form, InputGroup, Row } from 'react-bootstrap';
|
||||
import * as Icons from 'react-bootstrap-icons';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import './custom-datepicker.css';
|
||||
import { MiddlewareReturn } from '@floating-ui/core';
|
||||
import { MiddlewareState } from '@floating-ui/dom';
|
||||
import 'react-datepicker/dist/react-datepicker.css';
|
||||
import './custom-datepicker.scss';
|
||||
|
||||
type IconName = keyof typeof Icons;
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ const ModalMarkdown: React.FC<Props> = (props: Props): React.ReactNode => {
|
||||
<Markdown>{props.markdownText}</Markdown>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="primary" onClick={props.onHide}>
|
||||
<Button variant="outline-secondary" onClick={props.onHide}>
|
||||
Close
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
|
||||
@@ -43,10 +43,10 @@ function Sidebar(props: React.PropsWithChildren<Props>): React.ReactNode {
|
||||
<i className="bi bi-list"></i>
|
||||
</button>
|
||||
|
||||
<div className={`d-flex flex-column vh-100 bg-light sidebar ${props.isMobileOpen ? 'sidebar-mobile-open' : ''}`}>
|
||||
<div className={`d-flex flex-column vh-100 sidebar ${props.isMobileOpen ? 'sidebar-mobile-open' : ''}`}>
|
||||
<div className="sidebar-header plus-jakarta-sans-bold">
|
||||
<img src={`https://gravatar.com/avatar/${user?.gravatarImageUrl}.jpg`} alt="User icon" />
|
||||
<span>{user?.name ? user?.name : 'User'}</span>
|
||||
<span className="header-username">{user?.name ? user?.name : 'User'}</span>
|
||||
</div>
|
||||
|
||||
<div className="header-spacer"></div>
|
||||
@@ -115,10 +115,7 @@ function Sidebar(props: React.PropsWithChildren<Props>): React.ReactNode {
|
||||
</Nav>
|
||||
|
||||
{/* Footer at the bottom */}
|
||||
<div
|
||||
className="mt-auto text-center text-muted py-3"
|
||||
style={{ borderTop: '1px solid #ddd' }}
|
||||
>
|
||||
<div className="mt-auto text-center text-muted py-3">
|
||||
<small data-testid="footer-text">
|
||||
{build}
|
||||
</small>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
.sidebar {
|
||||
width: 276px;
|
||||
background-color: #fff;
|
||||
background-color: var(--bs-body-bg);
|
||||
position: fixed;
|
||||
left: 0;
|
||||
top: 0;
|
||||
@@ -40,10 +40,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.header-username {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
.header-spacer {
|
||||
margin-top: 1.8rem;
|
||||
margin-left: 1rem;
|
||||
border-bottom: 0.5px dashed #DCDCDC;
|
||||
border-bottom: 0.5px dashed var(--bs-header-sidebar-color);
|
||||
width: 220px;
|
||||
}
|
||||
.sidebar-header img {
|
||||
|
||||
@@ -1,20 +1,18 @@
|
||||
@import './theme.scss';
|
||||
@import '~bootstrap/scss/bootstrap';
|
||||
@import './theme.scss';
|
||||
|
||||
$light-bg: #E6F5E9;
|
||||
$dark-text: #343a40;
|
||||
|
||||
body {
|
||||
background-color: #E6F5E9;
|
||||
background-color: var(--bs-custom-body-bg);
|
||||
}
|
||||
|
||||
a, .btn-link {
|
||||
color: map-get($theme-colors, 'primary');
|
||||
color: var(--bs-primary);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover, .btn-link:hover {
|
||||
color: map-get($theme-colors, 'primary');
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
.poppins-thin {
|
||||
@@ -194,7 +192,7 @@ a:hover, .btn-link:hover {
|
||||
}
|
||||
|
||||
.home-productive {
|
||||
color: map-get($theme-colors, 'primary');
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
.font-size-14 {
|
||||
@@ -210,16 +208,28 @@ a:hover, .btn-link:hover {
|
||||
}
|
||||
|
||||
.home-new-item {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--bs-btn-new-btn-item-bg);
|
||||
border-radius: 3px;
|
||||
padding: 8px 16px;
|
||||
color: #fff;
|
||||
background-color: #233C46;
|
||||
color: var(--bs-btn-new-btn-item-color);
|
||||
background-color: var(--bs-btn-new-btn-item-bg);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.home-new-item svg {
|
||||
margin-right: 1rem;
|
||||
color: #4CD964;
|
||||
.home-new-item:hover {
|
||||
background-color: #36d451;
|
||||
}
|
||||
|
||||
.home-new-item-secondary {
|
||||
border: 1px solid #272d33;
|
||||
border-radius: 3px;
|
||||
padding: 8px 16px;
|
||||
color: #e0e0e0;
|
||||
background-color: #272d33;
|
||||
}
|
||||
|
||||
.home-new-item-secondary:hover {
|
||||
background-color: #333b42;
|
||||
}
|
||||
|
||||
.task-note-btn {
|
||||
@@ -227,17 +237,17 @@ a:hover, .btn-link:hover {
|
||||
}
|
||||
|
||||
pre {
|
||||
background-color: #f5f5f5;
|
||||
border: 1px solid #ddd;
|
||||
background-color: var(--bs-body-bg);
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
margin-top: 5px;
|
||||
overflow-x: auto; /* Enables horizontal scrolling for long lines */
|
||||
overflow-x: auto;
|
||||
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
tab-size: 2; /* Controls tab width */
|
||||
white-space: pre; /* Preserves whitespace */
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
code {
|
||||
@@ -253,3 +263,19 @@ code {
|
||||
border-radius: 3px;
|
||||
color: #d63384;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
border-right: none !important;
|
||||
padding: 0.375rem 0.05rem 0.375rem 0.75rem;
|
||||
}
|
||||
|
||||
.input-group > .form-control,
|
||||
.react-datepicker__input-container > .form-control {
|
||||
border-left: none !important;
|
||||
}
|
||||
|
||||
.dropdown-toggle.btn.btn-success {
|
||||
background-color: var(--bs-body-bg);
|
||||
color: var(--bs-body-color);
|
||||
border: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/* dark-mode.css */
|
||||
body.dark-mode {
|
||||
background-color: #121212;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.dark-mode .card {
|
||||
background-color: #1e1e1e;
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.dark-mode .form-control,
|
||||
.dark-mode .form-control-lg,
|
||||
.dark-mode .btn {
|
||||
background-color: #2c2c2c;
|
||||
color: #fff;
|
||||
border-color: #444;
|
||||
}
|
||||
|
||||
.dark-mode .btn-primary {
|
||||
background-color: #3f51b5;
|
||||
border-color: #3f51b5;
|
||||
}
|
||||
|
||||
.dark-mode .note-card {
|
||||
background-color: #1e1e1e;
|
||||
color: #fff;
|
||||
}
|
||||
@@ -4,12 +4,89 @@ $theme-colors: (
|
||||
'danger': #FB1A41, /* from design */
|
||||
);
|
||||
|
||||
$light-bg: #E6F5E9;
|
||||
$dark-text: #343a40;
|
||||
|
||||
$btn-border-radius: 0.3rem; // Default button border radius
|
||||
$btn-border-radius-sm: 0.3rem; // Small button border radius
|
||||
$btn-border-radius-lg: 0.3rem;
|
||||
|
||||
$input-border-radius: 0.3rem; // Default input border radius
|
||||
$input-border-radius-sm: 0.3rem; // Small input border radius
|
||||
$input-border-radius-lg: 0.3rem;
|
||||
$input-border-radius-lg: 0.3rem;
|
||||
|
||||
// Light theme overrides
|
||||
[data-bs-theme="light"] {
|
||||
/* Landing */
|
||||
--bs-landing-bg: url('../assets/landing-bg.jpg');
|
||||
--bs-bg-overlay: rgba(255, 255, 255, 0.85);
|
||||
|
||||
/* Body */
|
||||
--bs-custom-body-bg: #E6F5E9; // light green
|
||||
--bs-body-bg: rgba(248, 249, 250, 1);
|
||||
--bs-body-color: #212529;
|
||||
--bs-box-shadow: rgba(0, 0, 0, 0.1);
|
||||
|
||||
/* Card */
|
||||
--bs-card-bg: rgba(248, 249, 250, 1);
|
||||
--bs-card-color: #212529;
|
||||
|
||||
/* Border */
|
||||
--bs-border-color: #dee2e6;
|
||||
--bs-border-radius-lg: 4px;
|
||||
--bs-border-new-btn-item-color: #6c757d;
|
||||
--bs-border-color-translucent: rgba(248, 249, 250, 1);
|
||||
|
||||
/* Buttons */
|
||||
--bs-btn-color: #2EB745;
|
||||
--bs-btn-border-color: #2EB745;
|
||||
--bs-btn-hover-color: #2EB745;
|
||||
--bs-btn-new-btn-item-bg: #2EB745;
|
||||
--bs-btn-new-btn-item-color: #FFF;
|
||||
|
||||
/* Color palette */
|
||||
--bs-primary: #2EB745;
|
||||
--bs-info: #2EB745;
|
||||
|
||||
/* sidebar header */
|
||||
--bs-header-sidebar-color: #DCDCDC;
|
||||
}
|
||||
|
||||
// card bg color: #161a1d
|
||||
// secondary button bg: #272d33
|
||||
|
||||
// Dark theme overrides
|
||||
[data-bs-theme="dark"] {
|
||||
/* Landing */
|
||||
--bs-landing-bg: url('../assets/landing-bg-dark.jpg');
|
||||
--bs-bg-overlay: rgba(0, 0, 0, 0.95);
|
||||
|
||||
/* Body */
|
||||
--bs-custom-body-bg: #192123; // body background color
|
||||
--bs-body-bg: #161a1d; // input background color
|
||||
--bs-body-color: #e0e0e0;
|
||||
--bs-box-shadow: rgba(255, 255, 255, 0.9);
|
||||
|
||||
/* Card */
|
||||
--bs-card-bg: #161a1d; // #282d33
|
||||
--bs-card-color: #e0e0e0;
|
||||
|
||||
/* Border */
|
||||
--bs-border-color: #444;
|
||||
--bs-border-radius-lg: 4px;
|
||||
--bs-border-new-btn-item-color: #6c757d;
|
||||
--bs-border-color-translucent: #161a1d;
|
||||
|
||||
/* Buttons */
|
||||
--bs-btn-color: #2EB745;
|
||||
--bs-btn-border-color: #2EB745;
|
||||
--bs-btn-hover-color: #2EB745;
|
||||
--bs-btn-new-btn-item-bg: #2EB745;
|
||||
--bs-btn-new-btn-item-color: #212529;
|
||||
|
||||
/* Inputs */
|
||||
--bs-tertiary-bg: #161a1d;
|
||||
|
||||
/* Color palette */
|
||||
--bs-primary: #2EB745;
|
||||
--bs-info: #2EB745;
|
||||
|
||||
/* sidebar header */
|
||||
--bs-header-sidebar-color: #444;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { Col, Container, Row } from 'react-bootstrap';
|
||||
import { Card, Col, Container, Row } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import ContentHeader from '../../components/ContentHeader';
|
||||
|
||||
@@ -26,59 +26,65 @@ function About(): React.ReactNode {
|
||||
|
||||
<Row className="justify-content-center mb-4">
|
||||
<Col xs={12}>
|
||||
<div className="user-info-card">
|
||||
<h2 className="mb-4 poppins-bold about-title">{t('about_app_title')}</h2>
|
||||
<p className="poppins-medium">{t('about_app_description')}</p>
|
||||
<h4 className="mt-4 poppins-medium">{t('about_app_features')}</h4>
|
||||
<ul className="poppins-light">
|
||||
<li>{t('about_app_features_one')}</li>
|
||||
<li>{t('about_app_features_two')}</li>
|
||||
<li>{t('about_app_features_three')}</li>
|
||||
</ul>
|
||||
<h4 className="mt-4 poppins-medium">{t('about_app_help_title')}</h4>
|
||||
<p className="poppins-light">{t('about_app_help_description')}</p>
|
||||
</div>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<h2 className="mb-4 poppins-bold about-title">{t('about_app_title')}</h2>
|
||||
<p className="poppins-medium">{t('about_app_description')}</p>
|
||||
<h4 className="mt-4 poppins-medium">{t('about_app_features')}</h4>
|
||||
<ul className="poppins-light">
|
||||
<li>{t('about_app_features_one')}</li>
|
||||
<li>{t('about_app_features_two')}</li>
|
||||
<li>{t('about_app_features_three')}</li>
|
||||
</ul>
|
||||
<h4 className="mt-4 poppins-medium">{t('about_app_help_title')}</h4>
|
||||
<p className="poppins-light">{t('about_app_help_description')}</p>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row className="justify-content-center">
|
||||
<Col xs={12} md={8}>
|
||||
<div className="user-info-card min-height-350">
|
||||
<h2 className="mb-4 poppins-bold about-title">{t('about_tech_title')}</h2>
|
||||
<p className="poppins-light">{t('about_tech_description')}</p>
|
||||
<ul className="poppins-light">
|
||||
<li>{t('about_tech_list_one')}</li>
|
||||
<li>{t('about_tech_list_two')}</li>
|
||||
<li>{t('about_tech_list_three')}</li>
|
||||
<li>{t('about_tech_list_four')}</li>
|
||||
<li>{t('about_tech_list_five')}</li>
|
||||
<li>{t('about_tech_list_six')}</li>
|
||||
<li>{t('about_tech_list_seven')}</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Card className="min-height-350">
|
||||
<Card.Body>
|
||||
<h2 className="mb-4 poppins-bold about-title">{t('about_tech_title')}</h2>
|
||||
<p className="poppins-light">{t('about_tech_description')}</p>
|
||||
<ul className="poppins-light">
|
||||
<li>{t('about_tech_list_one')}</li>
|
||||
<li>{t('about_tech_list_two')}</li>
|
||||
<li>{t('about_tech_list_three')}</li>
|
||||
<li>{t('about_tech_list_four')}</li>
|
||||
<li>{t('about_tech_list_five')}</li>
|
||||
<li>{t('about_tech_list_six')}</li>
|
||||
<li>{t('about_tech_list_seven')}</li>
|
||||
</ul>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col sx={12} md={4} className="mt-4 mt-sm-0">
|
||||
<div className="user-info-card min-height-350">
|
||||
<h2 className="mb-4 poppins-bold about-title">{t('about_dev_title')}</h2>
|
||||
<p className="poppins-light">
|
||||
{t('about_dev_description')}
|
||||
<a href="mailto:ricardompcampos@gmail.com" className="text-decoration-none">
|
||||
ricardompcampos@gmail.com
|
||||
</a>
|
||||
{t('about_dev_description_two')}
|
||||
</p>
|
||||
<p className="poppins-light">
|
||||
You can also
|
||||
{' '}
|
||||
<a
|
||||
href="https://buy-me-a-coffee-two-nu.vercel.app/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Buy me a coffee
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<Card className="min-height-350">
|
||||
<Card.Body>
|
||||
<h2 className="mb-4 poppins-bold about-title">{t('about_dev_title')}</h2>
|
||||
<p className="poppins-light">
|
||||
{t('about_dev_description')}
|
||||
<a href="mailto:ricardompcampos@gmail.com" className="text-decoration-none">
|
||||
ricardompcampos@gmail.com
|
||||
</a>
|
||||
{t('about_dev_description_two')}
|
||||
</p>
|
||||
<p className="poppins-light">
|
||||
You can also
|
||||
{' '}
|
||||
<a
|
||||
href="https://buy-me-a-coffee-two-nu.vercel.app/"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Buy me a coffee
|
||||
</a>
|
||||
</p>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
+137
-149
@@ -1,7 +1,6 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import { Alert, Button, Col, Container, Form, Row } from 'react-bootstrap';
|
||||
import { Alert, Button, Card, Col, Container, Form, Row } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { ExclamationSquareFill, FileText, FloppyFill, Person, ShieldExclamation, TrashFill } from 'react-bootstrap-icons';
|
||||
import DOMPurify from 'dompurify';
|
||||
import { clearStorage, setDefaultLang } from '../../storage-service/storage';
|
||||
import AuthContext from '../../context/AuthContext';
|
||||
@@ -14,7 +13,6 @@ import { UserPatchRequest } from '../../types/UserPatchRequest';
|
||||
import { UserResponse } from '../../types/UserResponse';
|
||||
import ContentHeader from '../../components/ContentHeader';
|
||||
import AlertError from '../../components/AlertError';
|
||||
import './styles.css';
|
||||
|
||||
/**
|
||||
* Account page component.
|
||||
@@ -129,163 +127,153 @@ function Account(): React.ReactNode {
|
||||
|
||||
<Row>
|
||||
<Col xs={12} lg={6}>
|
||||
<div className="user-info-card">
|
||||
<div className="title">
|
||||
<Person />
|
||||
{' '}
|
||||
Change your info
|
||||
</div>
|
||||
<span className="description">
|
||||
Update only what you need. Blank fields will not be updated
|
||||
</span>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title>
|
||||
Update only what you need. Blank fields will not be updated
|
||||
</Card.Title>
|
||||
|
||||
<AlertError errorMessage={errorMessage} />
|
||||
<AlertError errorMessage={errorMessage} />
|
||||
|
||||
<Form noValidate validated={validated} onSubmit={handleSubmit} className="mt-4">
|
||||
{/* User name */}
|
||||
<FormInput
|
||||
labelText="First name"
|
||||
iconName="Person"
|
||||
required={false}
|
||||
name="name"
|
||||
placeholder={user?.name ? user.name : ''}
|
||||
value={userName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
<Form noValidate validated={validated} onSubmit={handleSubmit} className="mt-4">
|
||||
{/* User name */}
|
||||
<FormInput
|
||||
labelText="First name"
|
||||
iconName="Person"
|
||||
required={false}
|
||||
name="name"
|
||||
placeholder={user?.name ? user.name : ''}
|
||||
value={userName}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserName(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* User email */}
|
||||
<FormInput
|
||||
labelText="Email"
|
||||
iconName="At"
|
||||
required={false}
|
||||
name="email"
|
||||
placeholder={user?.email}
|
||||
value={userEmail}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserEmail(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{/* User email */}
|
||||
<FormInput
|
||||
labelText="Email"
|
||||
iconName="At"
|
||||
required={false}
|
||||
name="email"
|
||||
placeholder={user?.email}
|
||||
value={userEmail}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserEmail(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* User password */}
|
||||
<FormInput
|
||||
labelText="Password"
|
||||
iconName="Lock"
|
||||
required={false}
|
||||
type="password"
|
||||
name="password"
|
||||
value={userPassword}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserPassword(e.target.value);
|
||||
}}
|
||||
data_testid="account-password-one"
|
||||
/>
|
||||
{/* User password */}
|
||||
<FormInput
|
||||
labelText="Password"
|
||||
iconName="Lock"
|
||||
required={false}
|
||||
type="password"
|
||||
name="password"
|
||||
value={userPassword}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserPassword(e.target.value);
|
||||
}}
|
||||
data_testid="account-password-one"
|
||||
/>
|
||||
|
||||
{/* User password again */}
|
||||
<FormInput
|
||||
labelText="Repeat password"
|
||||
iconName="Lock"
|
||||
required={false}
|
||||
type="password"
|
||||
name="passwordAgain"
|
||||
value={userPasswordAgain}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserPasswordAgain(e.target.value);
|
||||
}}
|
||||
/>
|
||||
{/* User password again */}
|
||||
<FormInput
|
||||
labelText="Repeat password"
|
||||
iconName="Lock"
|
||||
required={false}
|
||||
type="password"
|
||||
name="passwordAgain"
|
||||
value={userPasswordAgain}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUserPasswordAgain(e.target.value);
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="d-grid">
|
||||
<button
|
||||
type="submit"
|
||||
className="home-new-item task-note-btn"
|
||||
>
|
||||
<FloppyFill size={25} />
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
<hr />
|
||||
<p>
|
||||
If you want to display your picture, we support Gravatar.
|
||||
Please head to
|
||||
{' '}
|
||||
<a href="https://gravatar.com" target="_blank" rel="noreferrer">Gravatar</a>
|
||||
{' '}
|
||||
to register or update your profile picture. Once updated, please wait a few
|
||||
minutes to see it here.
|
||||
</p>
|
||||
</div>
|
||||
<div className="d-grid">
|
||||
<button
|
||||
type="submit"
|
||||
className="home-new-item task-note-btn"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</Form>
|
||||
<hr />
|
||||
<p>
|
||||
If you want to display your picture, we support Gravatar.
|
||||
Please head to
|
||||
{' '}
|
||||
<a href="https://gravatar.com" target="_blank" rel="noreferrer">Gravatar</a>
|
||||
{' '}
|
||||
to register or update your profile picture. Once updated, please wait a few
|
||||
minutes to see it here.
|
||||
</p>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={12} lg={6} className="mt-4 mt-lg-0">
|
||||
<div className="user-info-card">
|
||||
<div className="title">
|
||||
<FileText />
|
||||
{' '}
|
||||
Change the app language
|
||||
</div>
|
||||
<span className="description">{t('account_app_lang_description')}</span>
|
||||
<div className="mt-4 mb-2">Available languages</div>
|
||||
<div>
|
||||
{languages.map((lang: LangAvailable) => (
|
||||
<Button
|
||||
key={lang.key}
|
||||
type="button"
|
||||
variant="outline-primary"
|
||||
className="btn-sm me-3 mb-3 d-block"
|
||||
onClick={() => handleLanguage(lang.lang)}
|
||||
data-testid={`language-button-${lang.lang}`}
|
||||
>
|
||||
{lang.lang === 'pt_br' && (
|
||||
<span>🇧🇷 </span>
|
||||
)}
|
||||
{lang.lang === 'en' && (
|
||||
<span>🇺🇸 </span>
|
||||
)}
|
||||
{lang.lang === 'es' && (
|
||||
<span>🇪🇸 </span>
|
||||
)}
|
||||
{lang.lang === 'ru' && (
|
||||
<span>🇷🇺 </span>
|
||||
)}
|
||||
{t(lang.key)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<Card>
|
||||
<Card.Body>
|
||||
<Card.Title>Change the app language</Card.Title>
|
||||
<span className="description">{t('account_app_lang_description')}</span>
|
||||
<div className="mt-4 mb-2">Available languages</div>
|
||||
<div>
|
||||
{languages.map((lang: LangAvailable) => (
|
||||
<Button
|
||||
key={lang.key}
|
||||
type="button"
|
||||
variant="outline-primary"
|
||||
className="btn-sm me-3 mb-3 d-block"
|
||||
onClick={() => handleLanguage(lang.lang)}
|
||||
data-testid={`language-button-${lang.lang}`}
|
||||
>
|
||||
{lang.lang === 'pt_br' && (
|
||||
<span>🇧🇷 </span>
|
||||
)}
|
||||
{lang.lang === 'en' && (
|
||||
<span>🇺🇸 </span>
|
||||
)}
|
||||
{lang.lang === 'es' && (
|
||||
<span>🇪🇸 </span>
|
||||
)}
|
||||
{lang.lang === 'ru' && (
|
||||
<span>🇷🇺 </span>
|
||||
)}
|
||||
{t(lang.key)}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
<div className="user-info-card mt-4">
|
||||
<div className="title">
|
||||
<ShieldExclamation />
|
||||
{' '}
|
||||
Your Privacy matters
|
||||
</div>
|
||||
<span className="description">
|
||||
You decide when to delete your data
|
||||
</span>
|
||||
<Card className="mt-4">
|
||||
<Card.Body>
|
||||
<Card.Title>Your Privacy matters</Card.Title>
|
||||
<span className="description">
|
||||
You decide when to delete your data
|
||||
</span>
|
||||
|
||||
<p className="mt-4 mb-2">{t('account_privacy_text')}</p>
|
||||
<Button
|
||||
variant="danger"
|
||||
type="button"
|
||||
onClick={() => setShowAlert(true)}
|
||||
className="text-white btn-icon-fix"
|
||||
>
|
||||
<TrashFill />
|
||||
{t('account_privacy_delete_btn')}
|
||||
</Button>
|
||||
<p className="mt-4 mb-2">{t('account_privacy_text')}</p>
|
||||
<Button
|
||||
variant="danger"
|
||||
type="button"
|
||||
onClick={() => setShowAlert(true)}
|
||||
className=""
|
||||
>
|
||||
{t('account_privacy_delete_btn')}
|
||||
</Button>
|
||||
|
||||
{showAlert && (
|
||||
<Alert className="mt-3" variant="danger" onClose={() => setShowAlert(false)} dismissible>
|
||||
<Alert.Heading>{t('account_delete_title')}</Alert.Heading>
|
||||
<p>{t('account_delete_description')}</p>
|
||||
<Button onClick={() => deleteAccount()} variant="outline-danger" className="btn-icon-fix">
|
||||
<ExclamationSquareFill />
|
||||
{t('account_delete_btn')}
|
||||
</Button>
|
||||
</Alert>
|
||||
)}
|
||||
</div>
|
||||
{showAlert && (
|
||||
<Alert className="mt-3" variant="danger" onClose={() => setShowAlert(false)} dismissible>
|
||||
<Alert.Heading>{t('account_delete_title')}</Alert.Heading>
|
||||
<p>{t('account_delete_description')}</p>
|
||||
<Button onClick={() => deleteAccount()} variant="outline-danger">
|
||||
{t('account_delete_btn')}
|
||||
</Button>
|
||||
</Alert>
|
||||
)}
|
||||
</Card.Body>
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
.user-info-card {
|
||||
background-color: #fff;
|
||||
padding: 2rem;
|
||||
border-radius: 4px;
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 12px;
|
||||
color: #6a8996;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-icon-fix svg {
|
||||
margin-right: 1rem;
|
||||
margin-top: -3px;
|
||||
}
|
||||
@@ -22,6 +22,7 @@ import TaskProgress from '../../components/TaskProgress';
|
||||
import AuthContext from '../../context/AuthContext';
|
||||
import ContentHeader from '../../components/ContentHeader';
|
||||
import AlertError from '../../components/AlertError';
|
||||
import { Search } from 'react-bootstrap-icons';
|
||||
|
||||
/**
|
||||
* Home page component.
|
||||
@@ -127,6 +128,9 @@ function Home(): React.ReactNode {
|
||||
|
||||
<Form noValidate validated={validated} onSubmit={handleSearch}>
|
||||
<InputGroup className="mb-3">
|
||||
<InputGroup.Text>
|
||||
<Search />
|
||||
</InputGroup.Text>
|
||||
<Form.Control
|
||||
pattern=".{3,}"
|
||||
required
|
||||
@@ -135,7 +139,7 @@ function Home(): React.ReactNode {
|
||||
name="search_term"
|
||||
placeholder={t('home_card_search_placeholder')}
|
||||
/>
|
||||
<Button type="submit" variant="primary" id="button-search">
|
||||
<Button type="submit" variant="outline-secondary" id="button-search">
|
||||
{t('home_card_search_btn')}
|
||||
</Button>
|
||||
</InputGroup>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
position: relative;
|
||||
text-align: center;
|
||||
color: $dark-text;
|
||||
background: url('../../assets/landing-bg.jpg') no-repeat center center;
|
||||
background: var(--bs-landing-bg) no-repeat center center;
|
||||
background-size: cover; // Ensures the image covers the whole background
|
||||
|
||||
&::before {
|
||||
@@ -14,7 +14,7 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.85); // Slight white overlay for text readability
|
||||
background-color: var(--bs-bg-overlay); // Slight overlay for text readability
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@@ -46,14 +46,14 @@
|
||||
margin-top: 5rem;
|
||||
font-size: 2.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
color: map-get($theme-colors, 'primary');
|
||||
color: var(--bs-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
a.login {
|
||||
&:hover {
|
||||
font-size: 2.7rem;
|
||||
color: darken(map-get($theme-colors, 'primary'), 10%);
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,11 +62,11 @@
|
||||
}
|
||||
|
||||
.btn-outline-primary {
|
||||
color: map-get($theme-colors, 'primary');
|
||||
border-color: map-get($theme-colors, 'primary');
|
||||
color: var(--bs-primary);
|
||||
border-color: var(--bs-primary);
|
||||
width: 9rem;
|
||||
&:hover {
|
||||
background-color: map-get($theme-colors, 'primary');
|
||||
background-color: var(--bs-primary);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
background: url('../../assets/landing-bg.jpg') no-repeat center center;
|
||||
background: var(--bs-landing-bg) no-repeat center center;
|
||||
background-size: cover;
|
||||
|
||||
&::before {
|
||||
@@ -15,7 +15,7 @@
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(255, 255, 255, 0.85); // White overlay for readability
|
||||
background-color: var(--bs-bg-overlay); // Slight overlay for text readability
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { NoteResponse } from '../../types/NoteResponse';
|
||||
import api from '../../api-service/api';
|
||||
import ApiConfig from '../../api-service/apiConfig';
|
||||
import { translateServerResponse } from '../../utils/TranslatorUtils';
|
||||
import { PlusCircleFill, ThreeDotsVertical } from 'react-bootstrap-icons';
|
||||
import { ThreeDotsVertical } from 'react-bootstrap-icons';
|
||||
import NoteTitle from '../../components/NoteTitle';
|
||||
import ModalMarkdown from '../../components/ModalMarkdown';
|
||||
import ContentHeader from '../../components/ContentHeader';
|
||||
@@ -197,7 +197,6 @@ function Note(): React.ReactNode {
|
||||
type="button"
|
||||
className="home-new-item task-note-btn"
|
||||
>
|
||||
<PlusCircleFill size={25} />
|
||||
Add Notes
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,3 @@
|
||||
.text-done {
|
||||
color: #ccc!important;
|
||||
}
|
||||
|
||||
.text-done .btn.btn-link {
|
||||
color: #ccc!important;
|
||||
}
|
||||
|
||||
.span-line-break {
|
||||
white-space: pre;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Container,
|
||||
@@ -17,7 +16,6 @@ import FormInput from '../../components/FormInput';
|
||||
import ModalMarkdown from '../../components/ModalMarkdown';
|
||||
import AlertError from '../../components/AlertError';
|
||||
import ContentHeader from '../../components/ContentHeader';
|
||||
import { FloppyFill } from 'react-bootstrap-icons';
|
||||
|
||||
type NoteAction = 'add' | 'edit';
|
||||
|
||||
@@ -273,19 +271,16 @@ function NoteAdd(): React.ReactNode {
|
||||
type="submit"
|
||||
className="home-new-item task-note-btn mt-3"
|
||||
>
|
||||
<FloppyFill size={25} />
|
||||
{t('note_form_submit')}
|
||||
</button>
|
||||
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
<button
|
||||
type="button"
|
||||
size="lg"
|
||||
className="ms-2"
|
||||
className="ms-2 home-new-item-secondary task-note-btn"
|
||||
onClick={() => navigate('/notes')}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</button>
|
||||
</Form>
|
||||
|
||||
</Card.Body>
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
Form,
|
||||
Row
|
||||
} from 'react-bootstrap';
|
||||
import { PlusCircleFill, ThreeDotsVertical } from 'react-bootstrap-icons';
|
||||
import { ThreeDotsVertical } from 'react-bootstrap-icons';
|
||||
import { TaskResponse } from '../../types/TaskResponse';
|
||||
import api from '../../api-service/api';
|
||||
import ApiConfig from '../../api-service/apiConfig';
|
||||
@@ -143,7 +143,6 @@ function Task(): React.ReactNode {
|
||||
const classesToUse: string[] = [];
|
||||
classesToUse.push('task-card');
|
||||
classesToUse.push('shadow-lg');
|
||||
classesToUse.push('bg-light');
|
||||
if (!task.done && task.highPriority) {
|
||||
classesToUse.push('high-importance');
|
||||
}
|
||||
@@ -191,7 +190,6 @@ function Task(): React.ReactNode {
|
||||
type="button"
|
||||
className="home-new-item task-note-btn"
|
||||
>
|
||||
<PlusCircleFill size={25} />
|
||||
Add Tasks
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
@import '../../styles/theme.scss';
|
||||
|
||||
.text-done {
|
||||
color: #ccc!important;
|
||||
}
|
||||
|
||||
.text-done .btn.btn-link {
|
||||
color: #ccc!important;
|
||||
}
|
||||
|
||||
.btn-action {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
@@ -18,13 +10,13 @@
|
||||
}
|
||||
|
||||
.normal-importance {
|
||||
border-left: 5px solid map-get($theme-colors, 'primary') !important;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
border-left: 5px solid var(--bs-primary) !important;
|
||||
box-shadow: 0 4px 8px var(--bs-box-shadow);
|
||||
}
|
||||
|
||||
.high-importance {
|
||||
border-left: 5px solid #FFD54F !important;
|
||||
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 4px 8px var(--bs-box-shadow);
|
||||
}
|
||||
|
||||
.high-importance:before {
|
||||
@@ -34,7 +26,7 @@
|
||||
left: 0;
|
||||
border-width: 20px 20px 0 0;
|
||||
border-style: solid;
|
||||
border-color: #FFD54F #ffffff;
|
||||
border-color: #FFD54F var(--bs-border-color);
|
||||
}
|
||||
|
||||
.done-no-importance {
|
||||
@@ -46,8 +38,8 @@
|
||||
}
|
||||
|
||||
.task-card-footer {
|
||||
background-color: #fff !important;
|
||||
border-top: #fff !important;
|
||||
background-color: var(--bs-card-bg) !important;
|
||||
border-top: var(--bs-card-bg) !important;
|
||||
}
|
||||
|
||||
/* Radio Buttons */
|
||||
@@ -85,13 +77,13 @@
|
||||
}
|
||||
|
||||
.custom-radio-button .form-check-input:checked {
|
||||
background-color: #0d6efd;
|
||||
border-color: #0d6efd;
|
||||
background-color: var(--bs-primary);
|
||||
border-color: var(--bs-primary);
|
||||
}
|
||||
|
||||
.custom-radio-button .form-check-input:focus {
|
||||
box-shadow: 0 0 0 0.25rem rgba(13, 110, 253, 0.25);
|
||||
border-color: #0d6efd;
|
||||
border-color: var(--bs-primary);
|
||||
}
|
||||
|
||||
.custom-radio-button .form-check-label {
|
||||
@@ -103,7 +95,7 @@
|
||||
}
|
||||
|
||||
.custom-radio-button:hover .form-check-label {
|
||||
color: #0d6efd;
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
/* Selected option text */
|
||||
@@ -123,6 +115,13 @@
|
||||
margin-top: -3px;
|
||||
}
|
||||
|
||||
.btn.btn-dark {
|
||||
position: fixed;
|
||||
top: 20px;
|
||||
right: 20px;
|
||||
z-index: 1030;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@media (max-width: 576px) {
|
||||
.radio-buttons-wrapper {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Col,
|
||||
Container,
|
||||
@@ -17,7 +16,6 @@ import { translateServerResponse } from '../../utils/TranslatorUtils';
|
||||
import FormInput from '../../components/FormInput';
|
||||
import ContentHeader from '../../components/ContentHeader';
|
||||
import AlertError from '../../components/AlertError';
|
||||
import { FloppyFill } from 'react-bootstrap-icons';
|
||||
|
||||
type TaskAction = 'add' | 'edit';
|
||||
|
||||
@@ -288,21 +286,18 @@ function TaskAdd(): React.ReactNode {
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
className="home-new-item task-note-btn mt-3"
|
||||
className="home-new-item task-note-btn"
|
||||
>
|
||||
<FloppyFill size={25} />
|
||||
{t('task_form_submit')}
|
||||
</button>
|
||||
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
<button
|
||||
type="button"
|
||||
size="lg"
|
||||
className="ms-2"
|
||||
className="ms-2 home-new-item-secondary task-note-btn"
|
||||
onClick={() => navigate('/tasks')}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</button>
|
||||
</Form>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user