@addev-be/ui 0.3.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/icons/circle-check.svg +1 -0
- package/assets/icons/circle-info.svg +1 -0
- package/assets/icons/circle-xmark.svg +1 -0
- package/assets/icons/triangle-exclamation.svg +1 -0
- package/package.json +1 -1
- package/src/Icons.tsx +12 -4
- package/src/components/auth/LoginForm.tsx +83 -0
- package/src/components/auth/LoginPage.tsx +32 -0
- package/src/components/auth/PasswordRecoveryForm.tsx +52 -0
- package/src/components/auth/PasswordResetForm.tsx +112 -0
- package/src/components/auth/index.ts +4 -0
- package/src/components/auth/styles.ts +14 -0
- package/src/components/forms/VerticalLabel.tsx +20 -0
- package/src/components/forms/index.ts +1 -1
- package/src/components/forms/styles.ts +9 -0
- package/src/components/index.ts +2 -0
- package/src/components/ui/Card/index.tsx +14 -0
- package/src/components/ui/Card/styles.ts +35 -0
- package/src/components/ui/Message/index.tsx +57 -0
- package/src/components/ui/Message/styles.ts +40 -0
- package/src/components/ui/index.ts +3 -0
- package/src/providers/ThemeProvider/ThemeProvider.ts +8 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-111 111-47-47c-9.4-9.4-24.6-9.4-33.9 0s-9.4 24.6 0 33.9l64 64c9.4 9.4 24.6 9.4 33.9 0L369 209z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM216 336c-13.3 0-24 10.7-24 24s10.7 24 24 24h80c13.3 0 24-10.7 24-24s-10.7-24-24-24h-8V248c0-13.3-10.7-24-24-24H216c-13.3 0-24 10.7-24 24s10.7 24 24 24h24v64H216zm40-144a32 32 0 1 0 0-64 32 32 0 1 0 0 64z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M256 48a208 208 0 1 1 0 416 208 208 0 1 1 0-416zm0 464A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM175 175c-9.4 9.4-9.4 24.6 0 33.9l47 47-47 47c-9.4 9.4-9.4 24.6 0 33.9s24.6 9.4 33.9 0l47-47 47 47c9.4 9.4 24.6 9.4 33.9 0s9.4-24.6 0-33.9l-47-47 47-47c9.4-9.4 9.4-24.6 0-33.9s-24.6-9.4-33.9 0l-47 47-47-47c-9.4-9.4-24.6-9.4-33.9 0z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--! Font Awesome Pro 6.5.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2023 Fonticons, Inc. --><path d="M248.4 84.3c1.6-2.7 4.5-4.3 7.6-4.3s6 1.6 7.6 4.3L461.9 410c1.4 2.3 2.1 4.9 2.1 7.5c0 8-6.5 14.5-14.5 14.5H62.5c-8 0-14.5-6.5-14.5-14.5c0-2.7 .7-5.3 2.1-7.5L248.4 84.3zm-41-25L9.1 385c-6 9.8-9.1 21-9.1 32.5C0 452 28 480 62.5 480h387c34.5 0 62.5-28 62.5-62.5c0-11.5-3.2-22.7-9.1-32.5L304.6 59.3C294.3 42.4 275.9 32 256 32s-38.3 10.4-48.6 27.3zM288 368a32 32 0 1 0 -64 0 32 32 0 1 0 64 0zm-8-184c0-13.3-10.7-24-24-24s-24 10.7-24 24v96c0 13.3 10.7 24 24 24s24-10.7 24-24V184z"/></svg>
|
package/package.json
CHANGED
package/src/Icons.tsx
CHANGED
|
@@ -10,6 +10,9 @@ import ArrowsRotateIcon from '../assets/icons/arrows-rotate.svg?react';
|
|
|
10
10
|
import ArrowsUpDownIcon from '../assets/icons/arrows-up-down.svg?react';
|
|
11
11
|
import CheckIcon from '../assets/icons/check.svg?react';
|
|
12
12
|
import ChevronDownIcon from '../assets/icons/chevron-down.svg?react';
|
|
13
|
+
import CircleCheckIcon from '../assets/icons/circle-check.svg?react';
|
|
14
|
+
import CircleInfoIcon from '../assets/icons/circle-info.svg?react';
|
|
15
|
+
import CircleXMarkIcon from '../assets/icons/circle-xmark.svg?react';
|
|
13
16
|
import CopyIcon from '../assets/icons/copy.svg?react';
|
|
14
17
|
import DownIcon from '../assets/icons/down.svg?react';
|
|
15
18
|
import EllipsisIcon from '../assets/icons/ellipsis.svg?react';
|
|
@@ -32,6 +35,7 @@ import TableFooterIcon from '../assets/icons/table-footer.svg?react';
|
|
|
32
35
|
import TableFooterSlashIcon from '../assets/icons/table-footer-slash.svg?react';
|
|
33
36
|
import TableIcon from '../assets/icons/table.svg?react';
|
|
34
37
|
import TallyIcon from '../assets/icons/tally.svg?react';
|
|
38
|
+
import TriangleExclamationIcon from '../assets/icons/triangle-exclamation.svg?react';
|
|
35
39
|
import UpIcon from '../assets/icons/up.svg?react';
|
|
36
40
|
import UserTieIcon from '../assets/icons/user-tie.svg?react';
|
|
37
41
|
import XBarIcon from '../assets/icons/x-bar.svg?react';
|
|
@@ -70,16 +74,17 @@ export const LoadingIcon: FC<IconFCProps> = ({ className, ...props }) => (
|
|
|
70
74
|
);
|
|
71
75
|
|
|
72
76
|
export {
|
|
73
|
-
ArrowDownAZIcon,
|
|
74
77
|
ArrowDown19Icon,
|
|
78
|
+
ArrowDownAZIcon,
|
|
75
79
|
ArrowDownBigSmallIcon,
|
|
76
|
-
ArrowUpZAIcon,
|
|
77
|
-
ArrowUpBigSmallIcon,
|
|
78
|
-
ArrowUp91Icon,
|
|
79
80
|
ArrowsRotateIcon,
|
|
80
81
|
ArrowsUpDownIcon,
|
|
82
|
+
ArrowUp91Icon,
|
|
83
|
+
ArrowUpBigSmallIcon,
|
|
84
|
+
ArrowUpZAIcon,
|
|
81
85
|
CheckIcon,
|
|
82
86
|
ChevronDownIcon,
|
|
87
|
+
CircleXMarkIcon,
|
|
83
88
|
CopyIcon,
|
|
84
89
|
DownIcon,
|
|
85
90
|
EllipsisIcon,
|
|
@@ -105,4 +110,7 @@ export {
|
|
|
105
110
|
UpIcon,
|
|
106
111
|
UserTieIcon,
|
|
107
112
|
XBarIcon,
|
|
113
|
+
CircleCheckIcon,
|
|
114
|
+
CircleInfoIcon,
|
|
115
|
+
TriangleExclamationIcon,
|
|
108
116
|
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Button, Input } from '../forms';
|
|
2
|
+
import { Card, Message } from '../ui';
|
|
3
|
+
import { FC, useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { Link, redirect } from 'react-router-dom';
|
|
5
|
+
|
|
6
|
+
import { FormContainer } from './styles';
|
|
7
|
+
import { StackedLabel } from '../forms/VerticalLabel';
|
|
8
|
+
import { useAuthentication } from '../../providers';
|
|
9
|
+
|
|
10
|
+
export const LoginForm: FC = () => {
|
|
11
|
+
const [username, setUsername] = useState(
|
|
12
|
+
localStorage.getItem('username') || ''
|
|
13
|
+
);
|
|
14
|
+
const [password, setPassword] = useState('');
|
|
15
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
16
|
+
const [error, setError] = useState('');
|
|
17
|
+
const usernameInputRef = useRef<HTMLInputElement>(null);
|
|
18
|
+
const passwordInputRef = useRef<HTMLInputElement>(null);
|
|
19
|
+
|
|
20
|
+
const { login } = useAuthentication();
|
|
21
|
+
|
|
22
|
+
const onLoginClicked = useCallback(() => {
|
|
23
|
+
setError('');
|
|
24
|
+
setIsLoading(true);
|
|
25
|
+
login(username, password).then((success) => {
|
|
26
|
+
setIsLoading(false);
|
|
27
|
+
if (success) {
|
|
28
|
+
redirect('/');
|
|
29
|
+
} else {
|
|
30
|
+
setError('Identifiants invalides');
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}, [login, password, username]);
|
|
34
|
+
|
|
35
|
+
useEffect(() => {
|
|
36
|
+
if (usernameInputRef.current && passwordInputRef.current) {
|
|
37
|
+
const input = !username
|
|
38
|
+
? usernameInputRef.current
|
|
39
|
+
: passwordInputRef.current;
|
|
40
|
+
input.select();
|
|
41
|
+
input.focus();
|
|
42
|
+
}
|
|
43
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
44
|
+
}, []);
|
|
45
|
+
|
|
46
|
+
if (isLoading) return <div>Chargement...</div>;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<Card>
|
|
50
|
+
<FormContainer>
|
|
51
|
+
<StackedLabel label="Adresse e-mail / Nom d'utilisateur">
|
|
52
|
+
<Input
|
|
53
|
+
ref={usernameInputRef}
|
|
54
|
+
type="email"
|
|
55
|
+
autoComplete="email"
|
|
56
|
+
required
|
|
57
|
+
value={username}
|
|
58
|
+
onChange={(e) => setUsername(e.target.value)}
|
|
59
|
+
/>
|
|
60
|
+
</StackedLabel>
|
|
61
|
+
|
|
62
|
+
<StackedLabel label="Mot de passe">
|
|
63
|
+
<Input
|
|
64
|
+
ref={passwordInputRef}
|
|
65
|
+
type="password"
|
|
66
|
+
autoComplete="current-password"
|
|
67
|
+
required
|
|
68
|
+
value={password}
|
|
69
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
70
|
+
/>
|
|
71
|
+
</StackedLabel>
|
|
72
|
+
|
|
73
|
+
{error && <Message type="error">{error}</Message>}
|
|
74
|
+
|
|
75
|
+
<Button color="primary" onClick={onLoginClicked}>
|
|
76
|
+
Se connecter
|
|
77
|
+
</Button>
|
|
78
|
+
|
|
79
|
+
<Link to="/recover">Mot de passe oublié ?</Link>
|
|
80
|
+
</FormContainer>
|
|
81
|
+
</Card>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { Outlet } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
export const LoginPage: FC = () => {
|
|
5
|
+
return (
|
|
6
|
+
<div className="flex flex-row h-full w-full">
|
|
7
|
+
<div className="relative hidden w-0 flex-1 lg:block">
|
|
8
|
+
<img className="absolute inset-0 h-full w-full object-cover" alt="" />
|
|
9
|
+
</div>
|
|
10
|
+
<div className="flex flex-1 flex-col justify-space-evenly overflow-y-auto px-4 py-12 sm:px-6 lg:flex-none lg:px-24 xl:px-36">
|
|
11
|
+
<div className="mx-auto w-full max-w-sm lg:max-w-96 lg:w-96 xl:max-w-[32rem] xl:w-[32rem]">
|
|
12
|
+
<div className="text-center">
|
|
13
|
+
<img
|
|
14
|
+
className="h-20 w-auto inline-block"
|
|
15
|
+
src="/logo192.png"
|
|
16
|
+
alt="Burotel"
|
|
17
|
+
/>
|
|
18
|
+
<h2 className="mt-8 text-2xl font-bold leading-9 tracking-tight text-gray-900">
|
|
19
|
+
Connectez-vous
|
|
20
|
+
</h2>
|
|
21
|
+
</div>
|
|
22
|
+
|
|
23
|
+
<div className="mt-10 sm:mx-auto sm:w-full sm:max-w-[640px]">
|
|
24
|
+
<div className="bg-white px-6 py-12 shadow sm:rounded-lg sm:px-12">
|
|
25
|
+
<Outlet />
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Button, Input } from '../forms';
|
|
2
|
+
import { FC, useCallback, useState } from 'react';
|
|
3
|
+
|
|
4
|
+
import { Card } from '../ui';
|
|
5
|
+
import { FormContainer } from './styles';
|
|
6
|
+
import { Link } from 'react-router-dom';
|
|
7
|
+
import { Message } from '../ui/Message';
|
|
8
|
+
import { StackedLabel } from '../forms/VerticalLabel';
|
|
9
|
+
import { useAuthentication } from '../../providers';
|
|
10
|
+
|
|
11
|
+
export const PasswordRecoveryForm: FC = () => {
|
|
12
|
+
const [email, setEmail] = useState('');
|
|
13
|
+
const [isSubmitted, setIsSubmitted] = useState(false);
|
|
14
|
+
const { sendRecoveryKey } = useAuthentication();
|
|
15
|
+
|
|
16
|
+
const onSubmitClicked = useCallback(() => {
|
|
17
|
+
sendRecoveryKey(email).then(() => {
|
|
18
|
+
setIsSubmitted(true);
|
|
19
|
+
});
|
|
20
|
+
}, [email, sendRecoveryKey]);
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<Card>
|
|
24
|
+
<FormContainer>
|
|
25
|
+
{isSubmitted ? (
|
|
26
|
+
<>
|
|
27
|
+
<Message type="success">
|
|
28
|
+
Un e-mail de réinitialisation de mot de passe vous a été envoyé.
|
|
29
|
+
</Message>
|
|
30
|
+
<Link to="/">Retour</Link>
|
|
31
|
+
</>
|
|
32
|
+
) : (
|
|
33
|
+
<>
|
|
34
|
+
<StackedLabel label="Adresse e-mail">
|
|
35
|
+
<Input
|
|
36
|
+
type="email"
|
|
37
|
+
autoComplete="email"
|
|
38
|
+
required
|
|
39
|
+
value={email}
|
|
40
|
+
onChange={(e) => setEmail(e.target.value)}
|
|
41
|
+
/>
|
|
42
|
+
</StackedLabel>
|
|
43
|
+
|
|
44
|
+
<Button color="primary" onClick={onSubmitClicked}>
|
|
45
|
+
Envoyer un lien de récupération
|
|
46
|
+
</Button>
|
|
47
|
+
</>
|
|
48
|
+
)}
|
|
49
|
+
</FormContainer>
|
|
50
|
+
</Card>
|
|
51
|
+
);
|
|
52
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Button, Card, Input, useAuthentication } from '@addev-be/ui';
|
|
2
|
+
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { Link, useParams } from 'react-router-dom';
|
|
4
|
+
|
|
5
|
+
import { FormContainer } from './styles';
|
|
6
|
+
import { Message } from '../ui/Message';
|
|
7
|
+
import { StackedLabel } from '../forms/VerticalLabel';
|
|
8
|
+
|
|
9
|
+
export const PasswordResetForm: FC = () => {
|
|
10
|
+
const { key } = useParams<{ key: string }>();
|
|
11
|
+
const [password1, setPassword1] = useState('');
|
|
12
|
+
const [password2, setPassword2] = useState('');
|
|
13
|
+
const [keyStatus, setKeyStatus] = useState(-1);
|
|
14
|
+
const [status, setStatus] = useState(-1);
|
|
15
|
+
const [error, setError] = useState('');
|
|
16
|
+
const { resetPassword, checkRecoveryKey } = useAuthentication();
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (key) {
|
|
20
|
+
checkRecoveryKey(key).then((status) => {
|
|
21
|
+
setKeyStatus(status);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}, [checkRecoveryKey, key]);
|
|
25
|
+
|
|
26
|
+
const onSubmitClicked = useCallback(() => {
|
|
27
|
+
if (key) {
|
|
28
|
+
if (password1 !== password2) {
|
|
29
|
+
setError('Les mots de passe ne sont pas identiques');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
resetPassword(key, password1).then((status) => {
|
|
33
|
+
setStatus(status);
|
|
34
|
+
setError(
|
|
35
|
+
status === 4
|
|
36
|
+
? 'Le mot de passe est trop faible (min. 8 caractères requis)'
|
|
37
|
+
: ''
|
|
38
|
+
);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
}, [key, password1, password2, resetPassword]);
|
|
42
|
+
|
|
43
|
+
const content = useMemo(() => {
|
|
44
|
+
if (keyStatus < 0) {
|
|
45
|
+
return <p>Chargement...</p>;
|
|
46
|
+
}
|
|
47
|
+
if (keyStatus > 0) {
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
<Message type="error">
|
|
51
|
+
La clé de récupération fournie est invalide ou expirée
|
|
52
|
+
</Message>
|
|
53
|
+
</>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
switch (status) {
|
|
58
|
+
case 0:
|
|
59
|
+
return (
|
|
60
|
+
<>
|
|
61
|
+
<Message type="success">
|
|
62
|
+
Votre mot de passe a été réinitialisé avec succès. Vous pouvez
|
|
63
|
+
maintenant vous connecter.
|
|
64
|
+
</Message>
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
default:
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}, [keyStatus, status]);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<Card>
|
|
75
|
+
<FormContainer>
|
|
76
|
+
{content ?? (
|
|
77
|
+
<>
|
|
78
|
+
<StackedLabel label="Nouveau mot de passe">
|
|
79
|
+
<Input
|
|
80
|
+
type="password"
|
|
81
|
+
autoComplete="current-password"
|
|
82
|
+
required
|
|
83
|
+
value={password1}
|
|
84
|
+
onChange={(e) => setPassword1(e.target.value)}
|
|
85
|
+
/>
|
|
86
|
+
</StackedLabel>
|
|
87
|
+
<StackedLabel label="Confirmation du mot de passe">
|
|
88
|
+
<Input
|
|
89
|
+
type="password"
|
|
90
|
+
autoComplete="current-password"
|
|
91
|
+
required
|
|
92
|
+
value={password2}
|
|
93
|
+
onChange={(e) => setPassword2(e.target.value)}
|
|
94
|
+
/>
|
|
95
|
+
</StackedLabel>
|
|
96
|
+
|
|
97
|
+
{error && (
|
|
98
|
+
<Message className="mt-4" type="error">
|
|
99
|
+
{error}
|
|
100
|
+
</Message>
|
|
101
|
+
)}
|
|
102
|
+
|
|
103
|
+
<Button color="primary" onClick={onSubmitClicked}>
|
|
104
|
+
Réinitialiser le mot de passe
|
|
105
|
+
</Button>
|
|
106
|
+
</>
|
|
107
|
+
)}
|
|
108
|
+
<Link to="/">Retour</Link>
|
|
109
|
+
</FormContainer>
|
|
110
|
+
</Card>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const FormContainer = styled.form.attrs({
|
|
4
|
+
className: 'FormContainer',
|
|
5
|
+
onSubmit: (e) => e.preventDefault(),
|
|
6
|
+
})`
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
gap: var(--space-4);
|
|
10
|
+
|
|
11
|
+
& > a {
|
|
12
|
+
text-align: center;
|
|
13
|
+
}
|
|
14
|
+
`;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { FC, HTMLAttributes } from 'react';
|
|
2
|
+
|
|
3
|
+
import { StackedLabelContainer } from './styles';
|
|
4
|
+
|
|
5
|
+
type StackedLabelProps = HTMLAttributes<HTMLDivElement> & {
|
|
6
|
+
label: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const StackedLabel: FC<StackedLabelProps> = ({
|
|
10
|
+
label,
|
|
11
|
+
children,
|
|
12
|
+
...props
|
|
13
|
+
}) => {
|
|
14
|
+
return (
|
|
15
|
+
<StackedLabelContainer {...props}>
|
|
16
|
+
<label>{label}</label>
|
|
17
|
+
{children}
|
|
18
|
+
</StackedLabelContainer>
|
|
19
|
+
);
|
|
20
|
+
};
|
|
@@ -20,3 +20,12 @@ export const Input = styled.input.attrs({
|
|
|
20
20
|
})`
|
|
21
21
|
${inputStyle}
|
|
22
22
|
`;
|
|
23
|
+
|
|
24
|
+
export const StackedLabelContainer = styled.div.attrs({
|
|
25
|
+
className: 'StackedLabelContainer',
|
|
26
|
+
})`
|
|
27
|
+
display: flex;
|
|
28
|
+
flex-direction: column;
|
|
29
|
+
gap: var(--space-1);
|
|
30
|
+
color: var(--color-gray-900);
|
|
31
|
+
`;
|
package/src/components/index.ts
CHANGED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CardContainer, CardFooter, CardHeader } from './styles';
|
|
2
|
+
import { FC, HTMLAttributes } from 'react';
|
|
3
|
+
|
|
4
|
+
type CardFC = FC<HTMLAttributes<HTMLDivElement>> & {
|
|
5
|
+
Header: typeof CardHeader;
|
|
6
|
+
Footer: typeof CardFooter;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const Card: CardFC = ({ children }) => {
|
|
10
|
+
return <CardContainer>{children}</CardContainer>;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
Card.Header = CardHeader;
|
|
14
|
+
Card.Footer = CardFooter;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const CardContainer = styled.div.attrs({
|
|
4
|
+
className: 'CardContainer',
|
|
5
|
+
})`
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
background-color: var(--color-white);
|
|
9
|
+
border-radius: var(--rounded-md);
|
|
10
|
+
box-shadow: var(--shadow-md);
|
|
11
|
+
padding: var(--space-4);
|
|
12
|
+
border: 1px solid var(--color-neutral-100);
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
export const CardHeader = styled.div.attrs({
|
|
16
|
+
className: 'CardHeader',
|
|
17
|
+
})`
|
|
18
|
+
display: flex;
|
|
19
|
+
flex-direction: row;
|
|
20
|
+
align-items: center;
|
|
21
|
+
padding-bottom: var(--space-2);
|
|
22
|
+
border-bottom: 1px solid var(--color-neutral-200);
|
|
23
|
+
margin-bottom: var(--space-2);
|
|
24
|
+
`;
|
|
25
|
+
|
|
26
|
+
export const CardFooter = styled.div.attrs({
|
|
27
|
+
className: 'CardFooter',
|
|
28
|
+
})`
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: row;
|
|
31
|
+
align-items: center;
|
|
32
|
+
padding-top: var(--space-2);
|
|
33
|
+
border-top: 1px solid var(--color-neutral-200);
|
|
34
|
+
margin-top: var(--space-2);
|
|
35
|
+
`;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import * as styles from './styles';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CircleCheckIcon,
|
|
5
|
+
CircleInfoIcon,
|
|
6
|
+
CircleXMarkIcon,
|
|
7
|
+
TriangleExclamationIcon,
|
|
8
|
+
} from '../../../Icons';
|
|
9
|
+
import { FC, PropsWithChildren } from 'react';
|
|
10
|
+
|
|
11
|
+
import { ThemeColor } from '../../../providers';
|
|
12
|
+
|
|
13
|
+
type MessageType = 'success' | 'info' | 'warning' | 'error';
|
|
14
|
+
|
|
15
|
+
type MessageProps = PropsWithChildren<{
|
|
16
|
+
className?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
type: MessageType;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
const colorsMap: Record<
|
|
22
|
+
MessageType,
|
|
23
|
+
{
|
|
24
|
+
baseColor: ThemeColor;
|
|
25
|
+
iconComponent: FC<{ className?: string }>;
|
|
26
|
+
}
|
|
27
|
+
> = {
|
|
28
|
+
success: {
|
|
29
|
+
baseColor: 'green',
|
|
30
|
+
iconComponent: CircleCheckIcon,
|
|
31
|
+
},
|
|
32
|
+
info: {
|
|
33
|
+
baseColor: 'sky',
|
|
34
|
+
iconComponent: CircleInfoIcon,
|
|
35
|
+
},
|
|
36
|
+
warning: {
|
|
37
|
+
baseColor: 'amber',
|
|
38
|
+
iconComponent: TriangleExclamationIcon,
|
|
39
|
+
},
|
|
40
|
+
error: {
|
|
41
|
+
baseColor: 'red',
|
|
42
|
+
iconComponent: CircleXMarkIcon,
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const Message: FC<MessageProps> = ({ children, title, type }) => {
|
|
47
|
+
const { iconComponent: Icon, baseColor = 'neutral' } = colorsMap[type];
|
|
48
|
+
return (
|
|
49
|
+
<styles.MessageContainer $baseColor={baseColor}>
|
|
50
|
+
<Icon className="MessageIcon" />
|
|
51
|
+
<styles.MessageContent>
|
|
52
|
+
{title && <h3>{title}</h3>}
|
|
53
|
+
<div>{children}</div>
|
|
54
|
+
</styles.MessageContent>
|
|
55
|
+
</styles.MessageContainer>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import styled, { css } from 'styled-components';
|
|
2
|
+
|
|
3
|
+
import { ThemeColor } from '../../../providers';
|
|
4
|
+
|
|
5
|
+
export const MessageContainer = styled.div.attrs({
|
|
6
|
+
className: 'MessageContainer',
|
|
7
|
+
})<{
|
|
8
|
+
$baseColor: ThemeColor;
|
|
9
|
+
}>`
|
|
10
|
+
display: flex;
|
|
11
|
+
flex-direction: row;
|
|
12
|
+
box-shadow: var(--shadow-md);
|
|
13
|
+
border-radius: var(--rounded-md);
|
|
14
|
+
padding: var(--space-4);
|
|
15
|
+
gap: var(--space-4);
|
|
16
|
+
|
|
17
|
+
${({ $baseColor }) => css`
|
|
18
|
+
border: 1px solid var(--color-${$baseColor}-300);
|
|
19
|
+
background-color: var(--color-${$baseColor}-100);
|
|
20
|
+
color: var(--color-${$baseColor}-600);
|
|
21
|
+
|
|
22
|
+
.MessageIcon {
|
|
23
|
+
fill: var(--color-${$baseColor}-600);
|
|
24
|
+
}
|
|
25
|
+
`}
|
|
26
|
+
|
|
27
|
+
.MessageIcon {
|
|
28
|
+
width: var(--space-5);
|
|
29
|
+
height: var(--space-5);
|
|
30
|
+
flex-shrink: 0;
|
|
31
|
+
}
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
export const MessageContent = styled.div.attrs({
|
|
35
|
+
className: 'MessageContent',
|
|
36
|
+
})`
|
|
37
|
+
display: flex;
|
|
38
|
+
flex-direction: column;
|
|
39
|
+
flex-grow: 1;
|
|
40
|
+
`;
|
|
@@ -52,4 +52,12 @@ export const ThemeProvider = styled.div<ThemeProviderProps>`
|
|
|
52
52
|
getThemeValuesCss('shadow', $theme.shadows),
|
|
53
53
|
].join('');
|
|
54
54
|
}}
|
|
55
|
+
|
|
56
|
+
a, a:visited {
|
|
57
|
+
color: var(--color-primary-500);
|
|
58
|
+
}
|
|
59
|
+
a:active,
|
|
60
|
+
a:hover {
|
|
61
|
+
color: var(--color-primary-700);
|
|
62
|
+
}
|
|
55
63
|
`;
|