Skip to content

Commit 9e95079

Browse files
authored
feat: Implement modal and button to create a vault (#59)
* feat: Create add vault button * feat: Create add vault modal * feat: link the modal and call the api * feat: update vaults list * fix: linter
1 parent ad8e6b6 commit 9e95079

File tree

3 files changed

+113
-9
lines changed

3 files changed

+113
-9
lines changed

src/app/ui/workspace/page.tsx

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import type { GetMyVaultsResponseDto } from '@shared/dto/responses/get-my-vaults
1212
import { useApi } from '@ui/hooks/useApi';
1313
import {
1414
Box,
15+
Button,
1516
Card,
1617
CardContent,
1718
CardHeader,
@@ -20,17 +21,19 @@ import {
2021
TextField,
2122
Typography,
2223
} from '@mui/material';
24+
import AddVaultModal from '@ui/components/modals/AddVaultModal';
2325

2426
export default function WorkspacePage(): JSX.Element {
2527
const [vaults, setVaults] = useState<VaultModelDto[]>([]);
2628
const [error, setError] = useState<Error | null>(null);
29+
const [open, setOpen] = useState(false);
2730
const vaultsGateway: VaultsGateway = container.resolve(VaultsGateway);
2831

2932
const { loading } = useApi<GetMyVaultsResponseDto>({
3033
request: () => vaultsGateway.getMyVaults(),
3134
onSuccess: data => setVaults(data.myVaults),
3235
onError: error => setError(error),
33-
deps: [],
36+
deps: [open],
3437
});
3538

3639
const [searchTerm, setSearchTerm] = useState('');
@@ -51,16 +54,31 @@ export default function WorkspacePage(): JSX.Element {
5154
gap: '3rem',
5255
}}
5356
>
57+
<AddVaultModal open={open} onClose={() => setOpen(false)} />
5458
<Typography variant={'h3'} textAlign={'left'}>
5559
My vaults
5660
</Typography>
57-
<TextField
58-
fullWidth
59-
placeholder="Search…"
60-
value={searchTerm}
61-
onChange={e => setSearchTerm(e.target.value)}
62-
sx={{ mb: 2 }}
63-
/>
61+
<Box
62+
sx={{
63+
display: 'flex',
64+
gap: '1rem',
65+
width: '100%',
66+
}}
67+
>
68+
<TextField
69+
fullWidth
70+
placeholder="Search…"
71+
value={searchTerm}
72+
onChange={e => setSearchTerm(e.target.value)}
73+
/>
74+
<Button
75+
variant="contained"
76+
sx={{ minWidth: 150 }}
77+
onClick={() => setOpen(true)}
78+
>
79+
Add a vault
80+
</Button>
81+
</Box>
6482
<ErrorMessage error={error} />
6583
<CircularLoader loading={loading} />
6684
{!loading && filteredVaults.length === 0 && (
@@ -74,7 +92,7 @@ export default function WorkspacePage(): JSX.Element {
7492
spacing={{ xs: 2, md: 3, lg: 3, xl: 4 }}
7593
columns={{ xs: 1, md: 2, lg: 3, xl: 3 }}
7694
overflow={'auto'}
77-
height={'70vh'}
95+
height={'65vh'}
7896
>
7997
{filteredVaults.map(vault => (
8098
<Grid key={vault.id} size={1}>
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import type { JSX } from 'react';
2+
import React, { useState } from 'react';
3+
import {
4+
Button,
5+
Dialog,
6+
DialogActions,
7+
DialogContent,
8+
DialogTitle,
9+
TextField,
10+
} from '@mui/material';
11+
import { VaultsGateway } from '@ui/gateways/vaults.gateway';
12+
import { container } from 'tsyringe';
13+
import type { CreateVaultRequestDto } from '@shared/dto/requests/create-vault.request.dto';
14+
import ErrorMessage from '@ui/components/common/ErrorMessage';
15+
16+
type AddVaultModalProps = {
17+
open: boolean;
18+
onClose: () => void;
19+
};
20+
21+
export default function AddVaultModal(props: AddVaultModalProps): JSX.Element {
22+
const [newLabel, setNewLabel] = useState<string>('');
23+
const [newSecret, setNewSecret] = useState<string>('');
24+
const [loading, setLoading] = useState<boolean>(false);
25+
const [error, setError] = useState<Error | null>(null);
26+
const vaultsGateway: VaultsGateway = container.resolve(VaultsGateway);
27+
28+
async function onCreate(data: CreateVaultRequestDto): Promise<void> {
29+
setLoading(true);
30+
try {
31+
await vaultsGateway.createVault(data);
32+
props.onClose();
33+
} catch (error) {
34+
if (error instanceof Error) setError(error);
35+
else console.error('Unhandled API error:', error);
36+
} finally {
37+
setLoading(false);
38+
}
39+
}
40+
41+
const handleConfirm = async (): Promise<void> => {
42+
await onCreate({ label: newLabel, secret: newSecret });
43+
};
44+
45+
return (
46+
<Dialog open={props.open} onClose={props.onClose}>
47+
<DialogTitle>Add a vault</DialogTitle>
48+
<DialogContent>
49+
<TextField
50+
autoFocus
51+
margin="dense"
52+
label="Label"
53+
fullWidth
54+
value={newLabel}
55+
onChange={e => setNewLabel(e.target.value)}
56+
/>
57+
<TextField
58+
margin="dense"
59+
label="Secret"
60+
fullWidth
61+
value={newSecret}
62+
onChange={e => setNewSecret(e.target.value)}
63+
sx={{ mt: 2 }}
64+
/>
65+
</DialogContent>
66+
<ErrorMessage error={error} />
67+
<DialogActions>
68+
<Button onClick={props.onClose}>Cancel</Button>
69+
<Button onClick={handleConfirm} variant="contained" loading={loading}>
70+
Create
71+
</Button>
72+
</DialogActions>
73+
</Dialog>
74+
);
75+
}

src/modules/ui/gateways/vaults.gateway.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { inject, injectable } from 'tsyringe';
22
import { GetMyVaultsResponseDto } from '@shared/dto/responses/get-my-vaults.response.dto';
33
import { LockliteApiRequestService } from '@ui/services/locklite-api-request.service';
4+
import { CreateVaultRequestDto } from '@shared/dto/requests/create-vault.request.dto';
5+
import { CreateVaultResponseDto } from '@shared/dto/responses/create-vault.response.dto';
46

57
@injectable()
68
export class VaultsGateway {
@@ -14,4 +16,13 @@ export class VaultsGateway {
1416
'/vaults'
1517
);
1618
}
19+
20+
public async createVault(
21+
data: CreateVaultRequestDto
22+
): Promise<CreateVaultResponseDto> {
23+
return await this._lockliteRequestService.post<CreateVaultResponseDto>(
24+
'/vaults',
25+
data
26+
);
27+
}
1728
}

0 commit comments

Comments
 (0)