chore: improve CI and fix navigation (#352)
* chore: simple change to trigger new ci * feat: make PR CD work issue #338 * ci: fix target deploy url * ci: improve deployments into just one matrix job * ci: put secret on env * fix: fix nagivation when creating notes and tasks from the home page * test: add sidebar test cases * chore: test files cleanup
This commit is contained in:
+33
-82
@@ -103,62 +103,6 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
client-dokploy:
|
||||
name: Deploy Client Changes to Stage
|
||||
runs-on: ubuntu-latest
|
||||
needs: client-docker-build
|
||||
if: github.event.pull_request.user.login == 'rmcampos' && github.event_name == 'pull_request'
|
||||
env:
|
||||
STAGE_DOMAIN: ${{ vars.STAGE_DOMAIN }}
|
||||
API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
|
||||
APP_ID: ${{ secrets.STAGE_WEB_CLIENT_ID }}
|
||||
steps:
|
||||
- name: Pre-deployment check
|
||||
run: |
|
||||
if ! curl -s -f "${STAGE_DOMAIN}/"; then
|
||||
echo "Stage environment is not healthy"
|
||||
else
|
||||
echo "Stage environment is healthy"
|
||||
fi
|
||||
|
||||
- name: Trigger Dokploy Deployment
|
||||
uses: nick-fields/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
response=$(curl -X POST \
|
||||
"${STAGE_DOMAIN}/api/application.deploy" \
|
||||
--max-time 30 \
|
||||
-H "accept: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"json\":{\"applicationId\":\"${APP_ID}\"}}" \
|
||||
-w "\n%{http_code}" \
|
||||
-s)
|
||||
|
||||
status_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
echo "Status code: $status_code"
|
||||
echo "Response body: $body"
|
||||
|
||||
if [ "$status_code" -ge 400 ]; then
|
||||
echo "Deployment failed with status code $status_code"
|
||||
exit 1
|
||||
fi
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
# Wait for deployment to complete
|
||||
sleep 30
|
||||
|
||||
if ! curl -s -f "${STAGE_DOMAIN}/"; then
|
||||
echo "Stage environment is not healthy"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Health check passed."
|
||||
|
||||
java-code-checks:
|
||||
needs: changes
|
||||
if: ${{ needs.changes.outputs.server == 'true' }}
|
||||
@@ -243,65 +187,72 @@ jobs:
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
server-dokploy:
|
||||
name: Deploy API Changes to Stage
|
||||
stage-deployments:
|
||||
name: Deploy Changes to Stage
|
||||
runs-on: ubuntu-latest
|
||||
needs: client-docker-build
|
||||
needs: [java-docker-build, client-docker-build]
|
||||
if: github.event.pull_request.user.login == 'rmcampos' && github.event_name == 'pull_request'
|
||||
env:
|
||||
STAGE_DOMAIN: ${{ vars.STAGE_DOMAIN }}
|
||||
STAGE_API_DOMAIN: ${{ vars.STAGE_API_DOMAIN }}
|
||||
DEPLOY_DOMAIN: ${{ vars.DEPLOY_DOMAIN }}
|
||||
API_KEY: ${{ secrets.DOKPLOY_API_KEY }}
|
||||
APP_ID: ${{ secrets.STAGE_API_CLIENT_ID }}
|
||||
SERVER_APPID: ${{ secrets.STAGE_API_CLIENT_ID }}
|
||||
CLIENT_APPID: ${{ secrets.STAGE_WEB_CLIENT_ID }}
|
||||
strategy:
|
||||
matrix:
|
||||
name: [server, client]
|
||||
include:
|
||||
- name: server
|
||||
health_check_url: "${{ vars.API_STAGE_URL }}/actuator/health"
|
||||
app_id: ${SERVER_APPID}
|
||||
- name: client
|
||||
health_check_url: "${{ vars.CLIENT_STAGE_URL }}/"
|
||||
app_id: ${CLIENT_APPID}
|
||||
steps:
|
||||
- name: Pre-deployment check
|
||||
run: |
|
||||
response=$(curl -s "${STAGE_API_DOMAIN}/actuator/health")
|
||||
status=$(echo "$response" | jq -r '.status')
|
||||
if [ "$status" != "UP" ]; then
|
||||
echo "Stage environment is not healthy. Status: $status"
|
||||
if ! curl -s -f "${{ matrix.health_check_url }}"; then
|
||||
echo "Stage environment is not healthy"
|
||||
else
|
||||
echo "Stage environment is healthy!"
|
||||
echo "Stage environment is healthy"
|
||||
fi
|
||||
|
||||
- name: Trigger Dokploy Deployment
|
||||
- name: Trigger Deployment
|
||||
uses: nick-fields/retry@v3.0.2
|
||||
with:
|
||||
timeout_minutes: 2
|
||||
max_attempts: 3
|
||||
command: |
|
||||
response=$(curl -X POST \
|
||||
"${STAGE_DOMAIN}/api/application.deploy" \
|
||||
"${DEPLOY_DOMAIN}/api/application.deploy" \
|
||||
--max-time 30 \
|
||||
-H "accept: application/json" \
|
||||
-H "x-api-key: ${API_KEY}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"json\":{\"applicationId\":\"${APP_ID}\"}}" \
|
||||
-d "{\"applicationId\":\"${{ matrix.app_id }}\"}" \
|
||||
-w "\n%{http_code}" \
|
||||
-s)
|
||||
|
||||
status_code=$(echo "$response" | tail -n1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
echo "Status code: $status_code"
|
||||
echo "Response body: $body"
|
||||
|
||||
if [ "$status_code" -ge 400 ]; then
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
echo "Deployment failed with status code $status_code"
|
||||
echo "Response body: $body"
|
||||
exit 1
|
||||
else
|
||||
echo "Deployment succeeded!"
|
||||
fi
|
||||
|
||||
- name: Verify deployment
|
||||
run: |
|
||||
# Wait for deployment to complete
|
||||
sleep 30
|
||||
|
||||
response=$(curl -s "${STAGE_API_DOMAIN}/actuator/health")
|
||||
status=$(echo "$response" | jq -r '.status')
|
||||
|
||||
if [ "$status" != "UP" ]; then
|
||||
echo "Stage environment is not healthy. Status: $status"
|
||||
echo "Full response: $response"
|
||||
if ! curl -s -f "${{ matrix.health_check_url }}"; then
|
||||
echo "Stage environment is not healthy"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Health check passed. Status: $status"
|
||||
else
|
||||
echo "Stage environment is healthy"
|
||||
fi
|
||||
+1
-1
@@ -26,7 +26,7 @@ function App(): React.ReactNode {
|
||||
const { signed, checkCurrentAuthUser } = useContext(AuthContext);
|
||||
|
||||
/**
|
||||
* Routes for users who are not signed in.
|
||||
* Routes for the users who are not signed in.
|
||||
* @type {RouteObject[]}
|
||||
*/
|
||||
const notSignedRouter: RouteObject[] = [
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import React from 'react';
|
||||
import { test } from 'vitest';
|
||||
import { test, vi } from 'vitest';
|
||||
import App from '../App';
|
||||
import { render } from '@testing-library/react';
|
||||
import AuthContext from '../context/AuthContext';
|
||||
import authContextMock from './__mocks__/authContextMock';
|
||||
import SidebarContext from '../context/SidebarContext';
|
||||
|
||||
const sidebarContextMock = {
|
||||
currentPage: '/home',
|
||||
setNewPage: vi.fn()
|
||||
};
|
||||
|
||||
test('Renders the app', () => {
|
||||
render(
|
||||
<AuthContext.Provider value={authContextMock}>
|
||||
<App />
|
||||
<SidebarContext.Provider value={sidebarContextMock}>
|
||||
<App />
|
||||
</SidebarContext.Provider>
|
||||
</AuthContext.Provider>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import ContentHeader from '../../components/ContentHeader';
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import Header from '../../components/Header';
|
||||
import '../../i18n';
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { MemoryRouter } from 'react-router';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import Sidebar from '../../components/Sidebar';
|
||||
import AuthContext from '../../context/AuthContext';
|
||||
import SidebarContext from '../../context/SidebarContext';
|
||||
import i18n from '../../i18n';
|
||||
|
||||
const authContextMock = {
|
||||
@@ -25,13 +26,20 @@ const authContextMock = {
|
||||
updateUser: vi.fn(),
|
||||
};
|
||||
|
||||
const sidebarContextMock = {
|
||||
currentPage: '/home',
|
||||
setNewPage: vi.fn()
|
||||
};
|
||||
|
||||
describe('Sidebar Component', () => {
|
||||
const renderSidebar = () => {
|
||||
return render(
|
||||
<MemoryRouter>
|
||||
<AuthContext.Provider value={authContextMock}>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<Sidebar />
|
||||
<SidebarContext.Provider value={sidebarContextMock}>
|
||||
<Sidebar />
|
||||
</SidebarContext.Provider>
|
||||
</I18nextProvider>
|
||||
</AuthContext.Provider>
|
||||
</MemoryRouter>
|
||||
|
||||
@@ -6,7 +6,6 @@ import userEvent from '@testing-library/user-event';
|
||||
import AuthProvider from '../../context/AuthProvider';
|
||||
import AuthContext, { AuthContextData } from '../../context/AuthContext';
|
||||
import api from '../../api-service/api';
|
||||
import ApiConfig from '../../api-service/apiConfig';
|
||||
import { API_TOKEN, USER_DATA } from '../../app-constants/app-constants';
|
||||
|
||||
// Mock the API service methods.
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// AuthProvider.test.tsx
|
||||
import React, { useContext } from 'react';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { render, act, waitFor, cleanup } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import SidebarProvider from '../../context/SidebarProvider';
|
||||
import SidebarContext, { SidebarContextData } from '../../context/SidebarContext';
|
||||
|
||||
// Create a helper component to consume AuthContext for testing.
|
||||
const ConsumerComponent: React.FC = () => {
|
||||
const {
|
||||
currentPage,
|
||||
setNewPage
|
||||
} = useContext<SidebarContextData>(SidebarContext);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div data-testid="page">{currentPage}</div>
|
||||
<button
|
||||
data-testid="setPage"
|
||||
onClick={() => {
|
||||
setNewPage('/another');
|
||||
}}
|
||||
>
|
||||
Change page
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
describe('SidebarProvider', () => {
|
||||
// Reset DOM and mocks for each test.
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
});
|
||||
|
||||
it('should render the default context values', () => {
|
||||
const { getByTestId } = render(
|
||||
<SidebarProvider>
|
||||
<ConsumerComponent />
|
||||
</SidebarProvider>
|
||||
);
|
||||
|
||||
expect(getByTestId('page').textContent).toBe('/home');
|
||||
});
|
||||
|
||||
it('should set a new page after click', async () => {
|
||||
const { getByTestId } = render(
|
||||
<SidebarProvider>
|
||||
<ConsumerComponent />
|
||||
</SidebarProvider>
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
userEvent.click(getByTestId('setPage'));
|
||||
});
|
||||
|
||||
await waitFor(() =>
|
||||
expect(getByTestId('page').textContent).toBe('/another')
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -11,6 +11,7 @@ const tasks: TaskResponse[] = [
|
||||
dueDate: '',
|
||||
dueDateFmt: '',
|
||||
lastUpdate: 'Moments ago',
|
||||
tag: 'test',
|
||||
urls: []
|
||||
}
|
||||
];
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import React from 'react';
|
||||
// import { MemoryRouter } from 'react-router';
|
||||
import { render } from '@testing-library/react';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import About from '../../views/About';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent, waitFor, getByTestId } from '@testing-library/react';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
@@ -35,7 +35,8 @@ const authContextMock = {
|
||||
name: 'Ricardo',
|
||||
email: 'test@example.com',
|
||||
admin: false,
|
||||
createdAt: new Date()
|
||||
createdAt: new Date(),
|
||||
gravatarImageUrl: 'http://image.com'
|
||||
},
|
||||
checkCurrentAuthUser: vi.fn(),
|
||||
signIn: vi.fn(),
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React from 'react';
|
||||
// import { MemoryRouter } from 'react-router';
|
||||
import { render } from '@testing-library/react';
|
||||
import { act, render, screen } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { SummaryResponse } from '../../types/SummaryResponse';
|
||||
import api from '../../api-service/api';
|
||||
import Home from '../../views/Home';
|
||||
import '../../i18n';
|
||||
import AuthContext from '../../context/AuthContext';
|
||||
@@ -21,30 +22,40 @@ const authContextMock = {
|
||||
name: 'Ricardo',
|
||||
email: 'ricardo@campos.com',
|
||||
admin: false,
|
||||
createdAt: new Date()
|
||||
createdAt: new Date(),
|
||||
gravatarImageUrl: 'http://image.com'
|
||||
},
|
||||
checkCurrentAuthUser: vi.fn(),
|
||||
signIn: vi.fn(),
|
||||
signOut: vi.fn(),
|
||||
register: vi.fn(),
|
||||
isAdmin: false,
|
||||
updateUser: vi.fn(),
|
||||
updateUser: vi.fn()
|
||||
};
|
||||
|
||||
describe('Renders the home view', () => {
|
||||
it('should render text based on new contentHeader component', () => {
|
||||
const { getByText } = render(
|
||||
<MemoryRouter>
|
||||
<AuthContext.Provider value={authContextMock}>
|
||||
<Home />
|
||||
</AuthContext.Provider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
it('should render text based on new contentHeader component', async () => {
|
||||
const mockData: SummaryResponse = {
|
||||
pendingTaskCount: 354,
|
||||
doneTaskCount: 555,
|
||||
notesCount: 2222
|
||||
};
|
||||
const mockedGetJSON = vi.spyOn(api, 'getJSON').mockResolvedValue(mockData);
|
||||
|
||||
expect(getByText('Hello,')).toBeDefined();
|
||||
expect(getByText('Ricardo')).toBeDefined();
|
||||
expect(getByText('Welcome to TaskNote! Get ready to complete your pending tasks')).toBeDefined();
|
||||
expect(getByText('Start Your Day, Be')).toBeDefined();
|
||||
expect(getByText('Productive')).toBeDefined();
|
||||
await act(async () => {
|
||||
render(
|
||||
<MemoryRouter>
|
||||
<AuthContext.Provider value={authContextMock}>
|
||||
<Home />
|
||||
</AuthContext.Provider>
|
||||
</MemoryRouter>
|
||||
);
|
||||
});
|
||||
|
||||
expect(screen.getByText('Hello,')).toBeDefined();
|
||||
expect(screen.getByText('Ricardo')).toBeDefined();
|
||||
expect(screen.getByText('Welcome to TaskNote! Get ready to complete your pending tasks')).toBeDefined();
|
||||
expect(screen.getByText('Start Your Day, Be')).toBeDefined();
|
||||
expect(screen.getByText('Productive')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent, waitFor, getByText, getByTestId } from '@testing-library/react';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
@@ -99,8 +99,8 @@ describe('NoteAdd Component', () => {
|
||||
it('should render text based on new contentHeader component', () => {
|
||||
const { getByText } = renderNoteAdd();
|
||||
|
||||
expect(getByText('All')).toBeDefined();
|
||||
expect(getByText('Notes')).toBeDefined();
|
||||
expect(getByText('Add')).toBeDefined();
|
||||
expect(getByText('Note')).toBeDefined();
|
||||
expect(getByText('Save your notes in plain text or Markdown format')).toBeDefined();
|
||||
expect(getByText('Create, Filter, and Easily Find')).toBeDefined();
|
||||
expect(getByText('Them')).toBeDefined();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React from 'react';
|
||||
import { render, fireEvent, waitFor, getByText } from '@testing-library/react';
|
||||
import { render, fireEvent, waitFor } from '@testing-library/react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { MemoryRouter } from 'react-router';
|
||||
import { I18nextProvider } from 'react-i18next';
|
||||
@@ -100,8 +100,8 @@ describe('TaskAdd Component', () => {
|
||||
it('should render text based on new contentHeader component', () => {
|
||||
const { getByText } = renderTaskAdd();
|
||||
|
||||
expect(getByText('All')).toBeDefined();
|
||||
expect(getByText('Tasks')).toBeDefined();
|
||||
expect(getByText('Add')).toBeDefined();
|
||||
expect(getByText('Task')).toBeDefined();
|
||||
expect(getByText('Be on top of your TODO list')).toBeDefined();
|
||||
expect(getByText('Create, Filter, and Easily Find')).toBeDefined();
|
||||
expect(getByText('Them')).toBeDefined();
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React from 'react';
|
||||
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';
|
||||
|
||||
type Props = {
|
||||
h1TextRegular: string;
|
||||
@@ -24,6 +25,8 @@ type Props = {
|
||||
* @returns {React.ReactNode} The rendered ContentHeader component.
|
||||
*/
|
||||
const ContentHeader: React.FC<Props> = (props: Props): React.ReactNode => {
|
||||
const { setNewPage } = useContext(SidebarContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="poppins-regular home-hello main-margin">
|
||||
@@ -47,22 +50,22 @@ const ContentHeader: React.FC<Props> = (props: Props): React.ReactNode => {
|
||||
</Col>
|
||||
{props.isHomeComponent && (
|
||||
<Col xs={4} className="text-end">
|
||||
<NavLink to="/tasks/new">
|
||||
<NavLink to="/tasks/new" onClick={() => setNewPage('/tasks/new')}>
|
||||
<button
|
||||
type="button"
|
||||
className="home-new-item w-45 mb-2"
|
||||
>
|
||||
<PlusCircleFill size={25} />
|
||||
Add note
|
||||
Add task
|
||||
</button>
|
||||
</NavLink>
|
||||
<NavLink to="/notes/new">
|
||||
<NavLink to="/notes/new" onClick={() => setNewPage('/notes/new')}>
|
||||
<button
|
||||
type="button"
|
||||
className="home-new-item w-45"
|
||||
>
|
||||
<PlusCircleFill size={25} />
|
||||
Add task
|
||||
Add note
|
||||
</button>
|
||||
</NavLink>
|
||||
</Col>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import React, { useContext, useEffect } from 'react';
|
||||
import { Nav } from 'react-bootstrap';
|
||||
import { NavLink } from 'react-router';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import AuthContext from '../../context/AuthContext';
|
||||
import SidebarContext from '../../context/SidebarContext';
|
||||
import NavButton from '../NavButton';
|
||||
import SidebarIcon from '../SidebarIcon';
|
||||
import { env } from '../../env';
|
||||
@@ -15,28 +16,12 @@ import './style.css';
|
||||
*/
|
||||
function Sidebar(): React.ReactNode {
|
||||
const { signOut, user } = useContext(AuthContext);
|
||||
const { currentPage, setNewPage } = useContext(SidebarContext);
|
||||
const { t } = useTranslation();
|
||||
const build = `Build: ${env.VITE_BUILD}`;
|
||||
const [current, setCurrent] = useState<string>('/home');
|
||||
|
||||
// Note: when selected, change class to plus-jakarta-sans-thin and add background
|
||||
|
||||
/**
|
||||
* Handles the sign-out action.
|
||||
*/
|
||||
const goOut = (): void => {
|
||||
signOut();
|
||||
};
|
||||
|
||||
/**
|
||||
* Handles the navigation link click event.
|
||||
*
|
||||
* @param {string} menu - The menu path.
|
||||
*/
|
||||
const navLinkClicked = (menu: string): void => {
|
||||
setCurrent(menu);
|
||||
};
|
||||
|
||||
useEffect(() => {}, [user]);
|
||||
|
||||
return (
|
||||
@@ -51,29 +36,29 @@ function Sidebar(): React.ReactNode {
|
||||
<div className="sidebar-menu-header plus-jakarta-sans-regular">Main Menu</div>
|
||||
|
||||
<Nav className="flex-column p-3 plus-jakarta-sans-thin">
|
||||
<NavLink to="/home" className="mb-2" onClick={() => navLinkClicked('/home')}>
|
||||
<div className={`sidebar-nav ${current === '/home' ? 'selected' : ''}`}>
|
||||
<NavLink to="/home" className="mb-2" onClick={() => setNewPage('/home')}>
|
||||
<div className={`sidebar-nav ${currentPage === '/home' ? 'selected' : ''}`}>
|
||||
<SidebarIcon
|
||||
iconName="dashboard"
|
||||
selected={current === '/home'}
|
||||
selected={currentPage === '/home'}
|
||||
/>
|
||||
Dashboard
|
||||
</div>
|
||||
</NavLink>
|
||||
<NavLink to="/tasks" className="mb-2" onClick={() => navLinkClicked('/tasks')}>
|
||||
<div className={`sidebar-nav ${current === '/tasks' ? 'selected' : ''}`}>
|
||||
<NavLink to="/tasks" className="mb-2" onClick={() => setNewPage('/tasks')}>
|
||||
<div className={`sidebar-nav ${currentPage.includes('/tasks') ? 'selected' : ''}`}>
|
||||
<SidebarIcon
|
||||
iconName="tasks"
|
||||
selected={current === '/tasks'}
|
||||
selected={currentPage.includes('/tasks')}
|
||||
/>
|
||||
{t('home_nav_tasks')}
|
||||
</div>
|
||||
</NavLink>
|
||||
<NavLink to="/notes" className="mb-2" onClick={() => navLinkClicked('/notes')}>
|
||||
<div className={`sidebar-nav ${current === '/notes' ? 'selected' : ''}`}>
|
||||
<NavLink to="/notes" className="mb-2" onClick={() => setNewPage('/notes')}>
|
||||
<div className={`sidebar-nav ${currentPage.includes('/notes') ? 'selected' : ''}`}>
|
||||
<SidebarIcon
|
||||
iconName="notes"
|
||||
selected={current === '/notes'}
|
||||
selected={currentPage.includes('/notes')}
|
||||
/>
|
||||
{t('home_nav_notes')}
|
||||
</div>
|
||||
@@ -83,25 +68,25 @@ function Sidebar(): React.ReactNode {
|
||||
<div className="sidebar-menu-header plus-jakarta-sans-regular">Preferences</div>
|
||||
|
||||
<Nav className="flex-column p-3 plus-jakarta-sans-thin">
|
||||
<NavLink to="/account" className="mb-2" onClick={() => navLinkClicked('/account')}>
|
||||
<div className={`sidebar-nav ${current === '/account' ? 'selected' : ''}`}>
|
||||
<NavLink to="/account" className="mb-2" onClick={() => setNewPage('/account')}>
|
||||
<div className={`sidebar-nav ${currentPage === '/account' ? 'selected' : ''}`}>
|
||||
<SidebarIcon
|
||||
iconName="account"
|
||||
selected={current === '/account'}
|
||||
selected={currentPage === '/account'}
|
||||
/>
|
||||
{t('footer_my_account')}
|
||||
</div>
|
||||
</NavLink>
|
||||
<NavLink to="/about" className="mb-2" onClick={() => navLinkClicked('/about')}>
|
||||
<div className={`sidebar-nav ${current === '/about' ? 'selected' : ''}`}>
|
||||
<NavLink to="/about" className="mb-2" onClick={() => setNewPage('/about')}>
|
||||
<div className={`sidebar-nav ${currentPage === '/about' ? 'selected' : ''}`}>
|
||||
<SidebarIcon
|
||||
iconName="about"
|
||||
selected={current === '/about'}
|
||||
selected={currentPage === '/about'}
|
||||
/>
|
||||
{t('home_nav_about')}
|
||||
</div>
|
||||
</NavLink>
|
||||
<NavButton className="mb-2" onClick={() => goOut()}>
|
||||
<NavButton className="mb-2" onClick={() => signOut()}>
|
||||
<div className="sidebar-nav">
|
||||
<SidebarIcon
|
||||
iconName="logout"
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
export interface SidebarContextData {
|
||||
currentPage: string;
|
||||
setNewPage: (page: string) => void;
|
||||
}
|
||||
|
||||
const SidebarContext = createContext<SidebarContextData>({} as SidebarContextData);
|
||||
|
||||
export default SidebarContext;
|
||||
@@ -0,0 +1,27 @@
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import SidebarContext, { SidebarContextData } from './SidebarContext';
|
||||
|
||||
interface Props {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const SidebarProvider: React.FC<{ children: React.ReactNode }> = ({ children }: Props) => {
|
||||
const [currentPage, setCurrentPage] = useState<string>('/home');
|
||||
|
||||
const setNewPage = (page: string): void => {
|
||||
setCurrentPage(page);
|
||||
};
|
||||
|
||||
const contextValue: SidebarContextData = useMemo(() => ({
|
||||
currentPage,
|
||||
setNewPage
|
||||
}), [currentPage, setNewPage]);
|
||||
|
||||
return (
|
||||
<SidebarContext.Provider value={contextValue}>
|
||||
{ children }
|
||||
</SidebarContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SidebarProvider;
|
||||
@@ -2,6 +2,7 @@ import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
import AuthProvider from './context/AuthProvider';
|
||||
import SidebarProvider from './context/SidebarProvider';
|
||||
import './i18n';
|
||||
|
||||
window.global ||= window;
|
||||
@@ -13,7 +14,9 @@ const root = createRoot(
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<AuthProvider>
|
||||
<App />
|
||||
<SidebarProvider>
|
||||
<App />
|
||||
</SidebarProvider>
|
||||
</AuthProvider>
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -188,8 +188,8 @@ function NoteAdd(): React.ReactNode {
|
||||
return (
|
||||
<Container>
|
||||
<ContentHeader
|
||||
h1TextRegular="All"
|
||||
h1TextBold="Notes"
|
||||
h1TextRegular="Add"
|
||||
h1TextBold="Note"
|
||||
subtitle="Save your notes in plain text or Markdown format"
|
||||
h2BlackText="Create, Filter, and Easily Find"
|
||||
h2GreenText="Them"
|
||||
|
||||
@@ -192,8 +192,8 @@ function TaskAdd(): React.ReactNode {
|
||||
return (
|
||||
<Container>
|
||||
<ContentHeader
|
||||
h1TextRegular="All"
|
||||
h1TextBold="Tasks"
|
||||
h1TextRegular="Add"
|
||||
h1TextBold="Task"
|
||||
subtitle="Be on top of your TODO list"
|
||||
h2BlackText="Create, Filter, and Easily Find"
|
||||
h2GreenText="Them"
|
||||
|
||||
+1
-1
@@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** This class contains resources for handling authentication. */
|
||||
/** This class contains the resources for handling authentication. */
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
@Tag(
|
||||
|
||||
Reference in New Issue
Block a user