Skip to content

Commit 0aac827

Browse files
authored
feat: Display the list of vaults (#57)
* refactor: Rename Home to HomePage * refactor: Create SharedChildrenProps & SharedLayoutProps * feat: Create workspace page * refactor: Use a base API URL in request service * feat: Create VaultsGateway * feat: Implement WorkspacePage * chore(linter): Add arrow parens rule * style(formatter): Format files by Prettier * fix: list display * feat: New vaults display * feat: New vaults display * feat: Improve vaults grid * feat: Improve vaults grid * fix: padding * fix: scrollable list * fix: scrollable secret * fix: list height * Revert "refactor: Use a base API URL in request service" This reverts commit 630b33f. * refactor: Transform request service to abstract class and create LockliteApiRequestService that extends it * fix: eslint * fix: redirect home page to workspace page
1 parent e01ba9c commit 0aac827

File tree

19 files changed

+222
-84
lines changed

19 files changed

+222
-84
lines changed

.idea/dictionaries/project.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.prettierignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
/node_modules/
22
/dist/
3+
/.github/release_template.md
34
README.md

.prettierrc

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
"semi": true,
33
"singleQuote": true,
44
"tabWidth": 2,
5-
"trailingComma": "es5"
5+
"trailingComma": "es5",
6+
"arrowParens": "avoid"
67
}

eslint.config.mjs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import {dirname} from 'path';
2-
import {fileURLToPath} from 'url';
3-
import {FlatCompat} from '@eslint/eslintrc';
1+
import { dirname } from 'path';
2+
import { fileURLToPath } from 'url';
3+
import { FlatCompat } from '@eslint/eslintrc';
44
import js from '@eslint/js';
55
import tseslint from 'typescript-eslint';
66
import eslintPluginPrettier from 'eslint-plugin-prettier';
@@ -50,26 +50,28 @@ export default tseslint.config(
5050
message: 'Do not use `as any`, types must be explicit and safe.',
5151
},
5252
{
53-
selector: "JSXOpeningElement[name.type='JSXIdentifier'][name.name=/^[a-z]/]",
53+
selector:
54+
"JSXOpeningElement[name.type='JSXIdentifier'][name.name=/^[a-z]/]",
5455
message:
5556
'Do not use native HTML elements: use an MUI component (PascalCase) instead.',
5657
},
5758
],
5859

5960
/* Code structure and clarity */
6061
// 'max-params': ['warn', 1],
62+
6163
'default-case': 'warn',
6264
'import/no-unresolved': 'error',
6365
'no-inline-comments': 'warn',
6466
'no-undefined': 'warn',
6567
'no-var': 'error',
66-
'prefer-const': ['error', {destructuring: 'all'}],
68+
'prefer-const': ['error', { destructuring: 'all' }],
6769
'require-await': 'error',
6870
'require-object-destructuring': 'off',
69-
71+
'arrow-parens': ['error', 'as-needed'],
7072
/* Formatting */
71-
'max-len': ['warn', {code: 300, ignoreUrls: true}],
72-
'prettier/prettier': ['warn', {semi: true}],
73+
'max-len': ['warn', { code: 300, ignoreUrls: true }],
74+
'prettier/prettier': ['warn', { semi: true }],
7375
semi: ['error', 'always'],
7476

7577
/* Naming conventions */
@@ -120,29 +122,29 @@ export default tseslint.config(
120122
],
121123
'@typescript-eslint/explicit-function-return-type': [
122124
'error',
123-
{allowExpressions: false},
125+
{ allowExpressions: false },
124126
],
125127
'@typescript-eslint/explicit-member-accessibility': [
126128
'error',
127-
{accessibility: 'explicit'},
129+
{ accessibility: 'explicit' },
128130
],
129131
'@typescript-eslint/explicit-module-boundary-types': 'error',
130132
'@typescript-eslint/no-empty-function': ['warn'],
131133
'@typescript-eslint/no-extraneous-class': [
132134
'error',
133-
{allowConstructorOnly: false},
135+
{ allowConstructorOnly: false },
134136
],
135137
'@typescript-eslint/no-explicit-any': 'error',
136138
'@typescript-eslint/no-inferrable-types': 'off',
137139
'@typescript-eslint/no-magic-numbers': [
138140
'warn',
139-
{ignoreEnums: true, ignore: [0, 1], enforceConst: true},
141+
{ ignoreEnums: true, ignore: [0, 1], enforceConst: true },
140142
],
141143
'@typescript-eslint/no-namespace': 'off',
142144
'@typescript-eslint/no-unsafe-member-access': 'warn',
143145
'@typescript-eslint/no-unused-vars': [
144146
'error',
145-
{argsIgnorePattern: '^_'},
147+
{ argsIgnorePattern: '^_' },
146148
],
147149
'@typescript-eslint/prefer-function-type': 'warn',
148150
'@typescript-eslint/prefer-readonly': 'warn',
@@ -187,9 +189,9 @@ export default tseslint.config(
187189

188190
{
189191
files: ['**/*.test.ts', '**/*.spec.ts', '**/*.test.tsx', '**/*.spec.tsx'],
190-
plugins: {jest: eslintPluginJest},
192+
plugins: { jest: eslintPluginJest },
191193
settings: {
192-
jest: {version: 29},
194+
jest: { version: 29 },
193195
},
194196
},
195197
],

src/app/api/layout.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { JSX } from 'react';
22
import React from 'react';
33
import type { Metadata } from 'next';
4+
import type { SharedLayoutProps } from '@shared/types/props/SharedLayoutProps';
45

56
export const metadata: Metadata = {
67
title: 'LockLite API',
@@ -9,9 +10,7 @@ export const metadata: Metadata = {
910

1011
export default function RootLayout({
1112
children,
12-
}: {
13-
children: React.ReactNode;
14-
}): JSX.Element {
13+
}: SharedLayoutProps): JSX.Element {
1514
return (
1615
// eslint-disable-next-line no-restricted-syntax
1716
<html lang="en">

src/app/ui/layout.tsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React from 'react';
44
import ThemeRegistry from '@ui/providers/ThemeRegistry';
55
import { AppBar, Container, Toolbar, Typography } from '@mui/material';
66
import PageContainer from '@ui/components/common/PageContainer';
7+
import type { SharedLayoutProps } from '@shared/types/props/SharedLayoutProps';
78

89
export const metadata: Metadata = {
910
title: {
@@ -15,9 +16,7 @@ export const metadata: Metadata = {
1516

1617
export default function RootLayout({
1718
children,
18-
}: {
19-
children: React.ReactNode;
20-
}): JSX.Element {
19+
}: SharedLayoutProps): JSX.Element {
2120
return (
2221
// eslint-disable-next-line no-restricted-syntax
2322
<html lang="en" style={{ height: '100%' }}>

src/app/ui/page.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import React from 'react';
21
import type { JSX } from 'react';
2+
import { redirect } from 'next/navigation';
33

4-
export default function Home(): JSX.Element {
5-
return <></>;
4+
export default function HomePage(): JSX.Element {
5+
redirect('/ui/workspace');
66
}

src/app/ui/workspace/layout.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import React from 'react';
2+
import type { JSX } from 'react';
3+
import type { SharedLayoutProps } from '@shared/types/props/SharedLayoutProps';
4+
5+
export default function WorkspaceLayout(props: SharedLayoutProps): JSX.Element {
6+
return <>{props.children}</>;
7+
}

src/app/ui/workspace/page.tsx

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
'use client';
2+
3+
import 'reflect-metadata';
4+
import React, { useState } from 'react';
5+
import type { JSX } from 'react';
6+
import type { VaultModelDto } from '@shared/dto/models/vault.model.dto';
7+
import ErrorMessage from '@ui/components/common/ErrorMessage';
8+
import CircularLoader from '@ui/components/common/CircularLoader';
9+
import { VaultsGateway } from '@ui/gateways/vaults.gateway';
10+
import { container } from 'tsyringe';
11+
import type { GetMyVaultsResponseDto } from '@shared/dto/responses/get-my-vaults.response.dto';
12+
import { useApi } from '@ui/hooks/useApi';
13+
import {
14+
Box,
15+
Card,
16+
CardContent,
17+
CardHeader,
18+
Container,
19+
Grid,
20+
Typography,
21+
} from '@mui/material';
22+
23+
export default function WorkspacePage(): JSX.Element {
24+
const [vaults, setVaults] = useState<VaultModelDto[]>([]);
25+
const [error, setError] = useState<Error | null>(null);
26+
const vaultsGateway: VaultsGateway = container.resolve(VaultsGateway);
27+
28+
const { loading } = useApi<GetMyVaultsResponseDto>({
29+
request: () => vaultsGateway.getMyVaults(),
30+
onSuccess: data => setVaults(data.myVaults),
31+
onError: error => setError(error),
32+
deps: [],
33+
});
34+
35+
return (
36+
<Container
37+
sx={{
38+
padding: '2rem',
39+
display: 'flex',
40+
flexDirection: 'column',
41+
gap: '3rem',
42+
}}
43+
>
44+
<Typography variant={'h3'} textAlign={'left'}>
45+
My vaults
46+
</Typography>
47+
<ErrorMessage error={error} />
48+
<CircularLoader loading={loading} />
49+
{!loading && vaults.length === 0 && (
50+
<Typography>No results found</Typography>
51+
)}
52+
{vaults.length > 0 && (
53+
<Grid
54+
container
55+
spacing={{ xs: 2, md: 3, lg: 3, xl: 4 }}
56+
columns={{ xs: 1, md: 2, lg: 3, xl: 3 }}
57+
overflow={'auto'}
58+
height={'70vh'}
59+
>
60+
{vaults.map(vault => (
61+
<Grid key={vault.id} size={1}>
62+
<Card
63+
sx={{
64+
bgcolor: 'background.paper',
65+
}}
66+
>
67+
<CardHeader title={vault.label} />
68+
<CardContent>
69+
<Box
70+
sx={{
71+
display: 'flex',
72+
alignItems: 'center',
73+
justifyContent: 'space-between',
74+
gap: '1rem',
75+
}}
76+
>
77+
<Typography variant="body2" color="text.secondary">
78+
Secret:
79+
</Typography>
80+
<Typography
81+
variant="body2"
82+
color="text.secondary"
83+
fontFamily={'monospace'}
84+
overflow={'scroll'}
85+
textOverflow={'ellipsis'}
86+
>
87+
{vault.secret}
88+
</Typography>
89+
</Box>
90+
</CardContent>
91+
</Card>
92+
</Grid>
93+
))}
94+
</Grid>
95+
)}
96+
</Container>
97+
);
98+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
export abstract class RequestService {
2+
protected constructor(private readonly _baseApiUrl: string = '') {}
3+
4+
private async _fetch<T>(uri: string, options: RequestInit): Promise<T> {
5+
const response: Response = await fetch(`${this._baseApiUrl}${uri}`, {
6+
...options,
7+
headers: {
8+
'Content-Type': 'application/json',
9+
...(options.headers ?? {}),
10+
},
11+
});
12+
13+
if (!response.ok) {
14+
let message: string = 'Unexpected error';
15+
try {
16+
// eslint-disable-next-line @typescript-eslint/typedef
17+
const errorJson = await response.json();
18+
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
19+
message = errorJson?.error ?? message;
20+
} catch {
21+
message = await response.text();
22+
}
23+
throw new Error(message);
24+
}
25+
26+
return response.json();
27+
}
28+
29+
public async get<T>(uri: string): Promise<T> {
30+
return await this._fetch<T>(uri, { method: 'GET' });
31+
}
32+
33+
public async post<T>(uri: string, body: unknown): Promise<T> {
34+
return await this._fetch<T>(uri, {
35+
method: 'POST',
36+
body: JSON.stringify(body),
37+
});
38+
}
39+
40+
public async put<T>(uri: string, body: unknown): Promise<T> {
41+
return await this._fetch<T>(uri, {
42+
method: 'PUT',
43+
body: JSON.stringify(body),
44+
});
45+
}
46+
47+
public async delete<T>(uri: string): Promise<T> {
48+
return await this._fetch<T>(uri, { method: 'DELETE' });
49+
}
50+
}

0 commit comments

Comments
 (0)