Skip to content

Commit d03b217

Browse files
Merge pull request #3 from alexcastrodev/refactor/dashboard
Refactor/dashboard
2 parents 982a45a + 0bbc2eb commit d03b217

File tree

35 files changed

+3865
-351
lines changed

35 files changed

+3865
-351
lines changed

backend/app/controllers/api/me/shortlinks_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def show
99
# GET /api/me/shortlinks
1010
def index
1111
render(
12-
json: ShortlinkSerializer.new(@current_user.shortlinks).serialize(meta: {
12+
json: ShortlinkSerializer.new(@current_user.shortlinks.order(created_at: :desc)).serialize(meta: {
1313
total: @current_user.shortlinks.count,
1414
}),
1515
status: :ok,

backend/app/models/shortlink.rb

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ def short_url
6464
"#{ENV["EDGE_API"]}/#{short_code}"
6565
end
6666

67-
6867
def cache_key
6968
"shortlink:#{short_code}"
7069
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import type { PropsWithChildren, ReactNode } from 'react';
2+
import { useGetLoggedUser } from '@internal/core/actions/get-logged-user/get-logged-user.hook';
3+
4+
interface AdminGuardProps {
5+
fallback?: ReactNode;
6+
}
7+
8+
export function AdminGuard({
9+
children,
10+
fallback = null,
11+
}: PropsWithChildren<AdminGuardProps>) {
12+
const { data } = useGetLoggedUser();
13+
14+
if (!data?.user?.admin) {
15+
return <>{fallback}</>;
16+
}
17+
18+
return <>{children}</>;
19+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { AdminGuard } from './admin-guard';
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
.header {
2+
display: none;
3+
position: sticky;
4+
top: 0;
5+
z-index: 100;
6+
background: var(--mantine-color-body);
7+
border-bottom: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
8+
padding: 0.75rem 0;
9+
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.05);
10+
}
11+
12+
.container {
13+
max-width: 1248px;
14+
}
15+
16+
.titleSection {
17+
display: flex;
18+
flex-direction: column;
19+
gap: 0.125rem;
20+
}
21+
22+
.title {
23+
font-size: 1.5rem;
24+
font-weight: 700;
25+
color: light-dark(var(--mantine-color-gray-9), var(--mantine-color-gray-1));
26+
margin: 0;
27+
line-height: 1.2;
28+
}
29+
30+
.subtitle {
31+
font-size: 0.875rem;
32+
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-gray-4));
33+
margin: 0;
34+
line-height: 1.2;
35+
}
36+
37+
.navButton {
38+
color: light-dark(var(--mantine-color-gray-7), var(--mantine-color-gray-3));
39+
font-weight: 500;
40+
}
41+
42+
.navButton:hover {
43+
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
44+
}
45+
46+
@media screen and (min-width: 640px) {
47+
.header {
48+
display: block;
49+
}
50+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import {
2+
Button,
3+
Menu,
4+
Image,
5+
Container,
6+
Group,
7+
Burger,
8+
Drawer,
9+
} from '@mantine/core';
10+
import { useDisclosure } from '@mantine/hooks';
11+
import { NavLink, useLocation, useNavigate } from 'react-router';
12+
import {
13+
IconHome2,
14+
IconLogout,
15+
IconChevronDown,
16+
IconUsers,
17+
} from '@tabler/icons-react';
18+
import { useTranslation } from 'react-i18next';
19+
import LogoDark from '/logo-dark.webp';
20+
import { useUserState } from '@internal/core/states/use-user-state';
21+
import { AdminGuard } from '../admin-guard';
22+
import styles from './app-header.module.css';
23+
24+
interface HeaderState {
25+
title?: string;
26+
subtitle?: string;
27+
showTitle?: boolean;
28+
}
29+
30+
export function AppHeader() {
31+
const navigate = useNavigate();
32+
const { t } = useTranslation('menu');
33+
const { clear } = useUserState();
34+
const { pathname } = useLocation();
35+
36+
const handleLogout = () => {
37+
clear();
38+
navigate('/login');
39+
};
40+
41+
const titles: Record<string, HeaderState> = {
42+
'/app': {
43+
title: t('dashboard'),
44+
subtitle: t('home_subtitle'),
45+
showTitle: true,
46+
},
47+
'/admin/users': {
48+
title: t('users'),
49+
subtitle: t('manage_users'),
50+
showTitle: true,
51+
},
52+
};
53+
54+
return (
55+
<header className={styles.header}>
56+
<Container size="xl" className={styles.container}>
57+
<Group justify="space-between" h="100%">
58+
<Group gap="lg">
59+
<Image src={LogoDark} w={60} h={40} fit="contain" />
60+
61+
<div className={styles.titleSection}>
62+
<h1 className={styles.title}>{titles[pathname]?.title}</h1>
63+
<p className={styles.subtitle}>{titles[pathname]?.subtitle}</p>
64+
</div>
65+
</Group>
66+
67+
<Group gap="md" visibleFrom="sm">
68+
<Button
69+
component={NavLink}
70+
to="/app"
71+
variant="subtle"
72+
leftSection={<IconHome2 size={18} stroke={1.5} />}
73+
className={styles.navButton}
74+
>
75+
{t('dashboard')}
76+
</Button>
77+
78+
<AdminGuard>
79+
<Menu shadow="md" width={200}>
80+
<Menu.Target>
81+
<Button
82+
variant="subtle"
83+
rightSection={<IconChevronDown size={18} stroke={1.5} />}
84+
className={styles.navButton}
85+
>
86+
{t('administration')}
87+
</Button>
88+
</Menu.Target>
89+
90+
<Menu.Dropdown>
91+
<Menu.Item
92+
component={NavLink}
93+
to="/admin/users"
94+
leftSection={<IconUsers size={16} stroke={1.5} />}
95+
>
96+
{t('users')}
97+
</Menu.Item>
98+
</Menu.Dropdown>
99+
</Menu>
100+
</AdminGuard>
101+
102+
<Button
103+
variant="subtle"
104+
onClick={handleLogout}
105+
color="red"
106+
leftSection={<IconLogout size={18} stroke={1.5} />}
107+
>
108+
{t('logout')}
109+
</Button>
110+
</Group>
111+
</Group>
112+
</Container>
113+
</header>
114+
);
115+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { AppHeader } from './app-header';
Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
.main {
2-
grid-area: main;
3-
padding: 10px;
4-
display: inline-block;
2+
flex: 1;
53
width: 100%;
6-
height: 100%;
74
overflow: auto;
5+
background: var(--mantine-color-body);
86
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export { MobileNav } from './mobile-nav';
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
.mobileNav {
2+
position: fixed;
3+
bottom: 0;
4+
left: 0;
5+
right: 0;
6+
display: flex;
7+
justify-content: space-around;
8+
align-items: center;
9+
background: light-dark(var(--mantine-color-white), var(--mantine-color-dark-7));
10+
border-top: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
11+
padding: 0.5rem 0;
12+
z-index: 100;
13+
box-shadow: 0 -2px 10px rgba(0, 0, 0, 0.05);
14+
backdrop-filter: blur(10px);
15+
}
16+
17+
@media (min-width: 640px) {
18+
.mobileNav {
19+
display: none;
20+
}
21+
}
22+
23+
.navItem {
24+
display: flex;
25+
flex-direction: column;
26+
align-items: center;
27+
justify-content: center;
28+
gap: 0.25rem;
29+
padding: 0.5rem 1rem;
30+
color: light-dark(var(--mantine-color-gray-6), var(--mantine-color-gray-5));
31+
text-decoration: none;
32+
transition: all 0.2s ease;
33+
border: none;
34+
background: transparent;
35+
cursor: pointer;
36+
flex: 1;
37+
max-width: 120px;
38+
border-radius: 0.5rem;
39+
}
40+
41+
.navItem:hover {
42+
color: light-dark(var(--mantine-color-blue-6), var(--mantine-color-blue-4));
43+
background: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-6));
44+
}
45+
46+
.navItem.active {
47+
color: light-dark(var(--mantine-color-blue-7), var(--mantine-color-blue-3));
48+
font-weight: 600;
49+
}
50+
51+
.label {
52+
font-size: 0.75rem;
53+
line-height: 1;
54+
}

0 commit comments

Comments
 (0)