mirror of
https://github.com/felipegcoutinho/openmonetis.git
synced 2026-05-09 11:01:45 +00:00
Compare commits
11 Commits
v2.1.0
...
49731238e4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
49731238e4 | ||
|
|
c5df97f7aa | ||
|
|
3476fda4db | ||
|
|
519b673ae5 | ||
|
|
303b8bedd4 | ||
|
|
f2b9b16896 | ||
|
|
6eba35542b | ||
|
|
f5e95ffba6 | ||
|
|
a75bb86eec | ||
|
|
a3b858621f | ||
|
|
fee2a2c9f5 |
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -12,7 +12,6 @@
|
||||
"**/.next": true,
|
||||
".next": true
|
||||
},
|
||||
"explorerExclude.backup": {},
|
||||
"editor.defaultFormatter": "biomejs.biome",
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
@@ -25,11 +24,9 @@
|
||||
"[json]": {
|
||||
"editor.defaultFormatter": "biomejs.biome"
|
||||
},
|
||||
"eslint.enable": false,
|
||||
"prettier.enable": false,
|
||||
"typescript.preferences.organizeImportsCollation": "ordinal",
|
||||
"editor.fontSize": 15,
|
||||
"[jsonc]": {
|
||||
"editor.defaultFormatter": "vscode.json-language-features"
|
||||
}
|
||||
}
|
||||
}
|
||||
21
CHANGELOG.md
21
CHANGELOG.md
@@ -7,6 +7,27 @@ e este projeto adere ao [Versionamento Semântico](https://semver.org/lang/pt-BR
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [2.1.1] - 2026-03-29
|
||||
|
||||
### Adicionado
|
||||
|
||||
- Navbar: novo componente `NavbarShell` que unifica a estrutura da barra de navegação entre o app e a landing page
|
||||
- UI: nova variante `navbar` no componente `Button`, centralizando os estilos de botões usados dentro da navbar
|
||||
- Analytics: integração com Umami self-hosted via script tag no layout raiz
|
||||
|
||||
### Alterado
|
||||
|
||||
- Navbar: `AnimatedThemeToggler` e `RefreshPageButton` passam a aceitar prop `variant` para adaptar estilos ao contexto (navbar ou sidebar)
|
||||
- Navbar: estilos inline duplicados de `nav-styles.ts` migrados para a variante `navbar` do Button
|
||||
- Logo: prop `showVersion` removida; prop `colorIcon` passa a aplicar filtro de cor também no variant `compact`
|
||||
- Scripts: `mockup` renomeado para `db:seed`; `db:enableExtensions` renomeado para `db:extensions`; script `dev-env` removido
|
||||
- Landing: `MobileNav` simplificado com a remoção da prop `triggerClassName`
|
||||
|
||||
### Removido
|
||||
|
||||
- Navbar: arquivo `nav-styles.ts` removido após migração dos estilos para a variante `navbar`
|
||||
- Dependências: `@vercel/analytics` e `@vercel/speed-insights` removidos (substituídos pelo Umami self-hosted)
|
||||
|
||||
## [2.1.0] - 2026-03-28
|
||||
|
||||
### Adicionado
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
> **⚠️ Não há versão online hospedada.** Você precisa clonar o repositório e rodar localmente ou no seu próprio servidor.
|
||||
|
||||
[](CHANGELOG.md)
|
||||
[](CHANGELOG.md)
|
||||
[](https://nextjs.org/)
|
||||
[](https://www.typescriptlang.org/)
|
||||
[](https://www.postgresql.org/)
|
||||
@@ -156,7 +156,7 @@ O script irá:
|
||||
|
||||
```bash
|
||||
docker compose up db -d
|
||||
pnpm db:enableExtensions
|
||||
pnpm db:extensions
|
||||
```
|
||||
|
||||
4. **Execute as migrations e inicie**
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
{
|
||||
"name": "openmonetis",
|
||||
"version": "2.1.0",
|
||||
"version": "2.1.1",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack",
|
||||
"dev-env": "tsx scripts/dev.ts",
|
||||
"mockup": "tsx scripts/mock-data.ts",
|
||||
"db:seed": "tsx scripts/mock-data.ts",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "biome check .",
|
||||
@@ -14,7 +13,7 @@
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:push": "drizzle-kit push",
|
||||
"db:enableExtensions": "tsx scripts/postgres/enable-extensions.ts",
|
||||
"db:extensions": "tsx scripts/postgres/enable-extensions.ts",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"docker:up": "docker compose up --build",
|
||||
"docker:up:db": "docker compose up -d db",
|
||||
@@ -61,8 +60,6 @@
|
||||
"@remixicon/react": "4.9.0",
|
||||
"@tanstack/react-table": "8.21.3",
|
||||
"@tanstack/react-virtual": "^3.13.23",
|
||||
"@vercel/analytics": "^2.0.1",
|
||||
"@vercel/speed-insights": "^2.0.0",
|
||||
"ai": "^6.0.141",
|
||||
"better-auth": "1.5.6",
|
||||
"canvas-confetti": "^1.9.4",
|
||||
|
||||
71
pnpm-lock.yaml
generated
71
pnpm-lock.yaml
generated
@@ -104,12 +104,6 @@ importers:
|
||||
'@tanstack/react-virtual':
|
||||
specifier: ^3.13.23
|
||||
version: 3.13.23(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
'@vercel/analytics':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
|
||||
'@vercel/speed-insights':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)
|
||||
ai:
|
||||
specifier: ^6.0.141
|
||||
version: 6.0.141(zod@4.3.6)
|
||||
@@ -2666,65 +2660,10 @@ packages:
|
||||
'@types/whatwg-url@13.0.0':
|
||||
resolution: {integrity: sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==}
|
||||
|
||||
'@vercel/analytics@2.0.1':
|
||||
resolution: {integrity: sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==}
|
||||
peerDependencies:
|
||||
'@remix-run/react': ^2
|
||||
'@sveltejs/kit': ^1 || ^2
|
||||
next: '>= 13'
|
||||
nuxt: '>= 3'
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
svelte: '>= 4'
|
||||
vue: ^3
|
||||
vue-router: ^4
|
||||
peerDependenciesMeta:
|
||||
'@remix-run/react':
|
||||
optional: true
|
||||
'@sveltejs/kit':
|
||||
optional: true
|
||||
next:
|
||||
optional: true
|
||||
nuxt:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
svelte:
|
||||
optional: true
|
||||
vue:
|
||||
optional: true
|
||||
vue-router:
|
||||
optional: true
|
||||
|
||||
'@vercel/oidc@3.1.0':
|
||||
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
'@vercel/speed-insights@2.0.0':
|
||||
resolution: {integrity: sha512-jwkNcrTeafWxjmWq4AHBaptSqZiJkYU5adLC9QBSqeim0GcqDMgN5Ievh8OG1rJ6W3A4l1oiP7qr9CWxGuzu3w==}
|
||||
peerDependencies:
|
||||
'@sveltejs/kit': ^1 || ^2
|
||||
next: '>= 13'
|
||||
nuxt: '>= 3'
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
svelte: '>= 4'
|
||||
vue: ^3
|
||||
vue-router: ^4
|
||||
peerDependenciesMeta:
|
||||
'@sveltejs/kit':
|
||||
optional: true
|
||||
next:
|
||||
optional: true
|
||||
nuxt:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
svelte:
|
||||
optional: true
|
||||
vue:
|
||||
optional: true
|
||||
vue-router:
|
||||
optional: true
|
||||
|
||||
adler-32@1.3.1:
|
||||
resolution: {integrity: sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==}
|
||||
engines: {node: '>=0.8'}
|
||||
@@ -6557,18 +6496,8 @@ snapshots:
|
||||
'@types/webidl-conversions': 7.0.3
|
||||
optional: true
|
||||
|
||||
'@vercel/analytics@2.0.1(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)':
|
||||
optionalDependencies:
|
||||
next: 16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react: 19.2.4
|
||||
|
||||
'@vercel/oidc@3.1.0': {}
|
||||
|
||||
'@vercel/speed-insights@2.0.0(next@16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4))(react@19.2.4)':
|
||||
optionalDependencies:
|
||||
next: 16.1.7(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)
|
||||
react: 19.2.4
|
||||
|
||||
adler-32@1.3.1: {}
|
||||
|
||||
ai@6.0.141(zod@4.3.6):
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 4.0 KiB |
39
setup.mjs
39
setup.mjs
@@ -21,6 +21,7 @@ const c = {
|
||||
red: "\x1b[31m",
|
||||
yellow: "\x1b[33m",
|
||||
cyan: "\x1b[36m",
|
||||
orange: "\x1b[38;5;214m",
|
||||
};
|
||||
|
||||
const sym = {
|
||||
@@ -81,10 +82,38 @@ function abort(msg) {
|
||||
|
||||
// ─── Header ──────────────────────────────────────────────────────────────────
|
||||
|
||||
console.log(`
|
||||
${c.bold}${c.cyan} OpenMonetis — Setup${c.reset}
|
||||
${c.dim}Gestão financeira self-hosted${c.reset}
|
||||
`);
|
||||
const logoLines = [
|
||||
".............................+@@@@@@@@@@=.............................",
|
||||
".............................@@@@@@@@@@@:.............................",
|
||||
"...................+@@@@@@*-:@@@@@@@@@@%...=@@@@@@-...................",
|
||||
"..................@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@%..................",
|
||||
"................=@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@+................",
|
||||
"...................-=+%@@@@@@@@@@@@@@@@@@@@@*:........................",
|
||||
".......................#@@@@@@@@@@@@@@@@@@@@@@@+......................",
|
||||
"....................%@@@@@@@@@@@@@%#@@@@@@@@@@@@*.....................",
|
||||
"....................+@@@@@@@@@@@......*@@@@@@#........................",
|
||||
".........................:#@@=...........+#...........................",
|
||||
];
|
||||
|
||||
const nameLines = [
|
||||
" ___ __ __ _ _ ",
|
||||
" / _ \\ _ __ ___ _ __ | \\/ | ___ _ __ ___| |_(_)___ ",
|
||||
" | | | | '_ \\ / _ \\ '_ \\| |\\/| |/ _ \\| '_ \\ / _ \\ __| / __|",
|
||||
" | |_| | |_) | __/ | | | | | | (_) | | | | __/ |_| \\__ \\",
|
||||
" \\___/| .__/ \\___|_| |_|_| |_|\\___/|_| |_|\\___|\\__|_|___/",
|
||||
" |_| ",
|
||||
];
|
||||
|
||||
const nameStart = Math.floor((logoLines.length - nameLines.length) / 2);
|
||||
|
||||
console.log();
|
||||
for (let i = 0; i < logoLines.length; i++) {
|
||||
const logoCol = c.orange + logoLines[i].replaceAll(".", " ").substring(14, 56).padEnd(42) + c.reset;
|
||||
const nameIdx = i - nameStart;
|
||||
const nameCol = nameIdx >= 0 && nameIdx < nameLines.length ? nameLines[nameIdx] : "";
|
||||
console.log(logoCol + " " + nameCol);
|
||||
}
|
||||
console.log(`\n${" ".repeat(46)}${c.dim}Gestão financeira · self-hosted${c.reset}\n`);
|
||||
|
||||
// ─── ETAPA 1: Verificações do sistema ────────────────────────────────────────
|
||||
|
||||
@@ -329,7 +358,7 @@ if (useLocalDocker) {
|
||||
// Extensões
|
||||
s = spinner("Habilitando extensões do banco...");
|
||||
try {
|
||||
run("pnpm db:enableExtensions", { cwd: targetDir });
|
||||
run("pnpm db:extensions", { cwd: targetDir });
|
||||
s.stop("Extensões habilitadas");
|
||||
} catch {
|
||||
s.fail("Falha ao habilitar extensões");
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
extraFeatures,
|
||||
getMetricsItems,
|
||||
mainFeatures,
|
||||
navbarActionClassName,
|
||||
navLinks,
|
||||
pwaHighlights,
|
||||
stackItems,
|
||||
@@ -27,6 +26,7 @@ import { landingImages } from "@/features/landing/images";
|
||||
import { fetchGitHubStats } from "@/features/landing/queries";
|
||||
import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler";
|
||||
import { Logo } from "@/shared/components/logo";
|
||||
import { NavbarShell } from "@/shared/components/navigation/navbar/navbar-shell";
|
||||
import { Badge } from "@/shared/components/ui/badge";
|
||||
import { Button } from "@/shared/components/ui/button";
|
||||
import { Card, CardContent } from "@/shared/components/ui/card";
|
||||
@@ -50,65 +50,60 @@ export default async function Page() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col">
|
||||
{/* Navigation */}
|
||||
<header className="sticky top-0 z-50 flex h-16 shrink-0 items-center bg-primary">
|
||||
<div className="relative z-10 max-w-8xl mx-auto px-4 w-full flex h-full items-center justify-between">
|
||||
<Logo variant="compact" invertTextOnDark={false} />
|
||||
<NavbarShell>
|
||||
{/* Center Navigation Links */}
|
||||
<nav className="hidden md:flex items-center gap-1 absolute left-1/2 -translate-x-1/2">
|
||||
{navLinks.map(({ href, label }) => (
|
||||
<a
|
||||
key={href}
|
||||
href={href}
|
||||
className="rounded-md px-2 py-1.5 text-sm font-medium text-black/75 hover:text-black hover:bg-black/10 transition-colors"
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Center Navigation Links */}
|
||||
<nav className="hidden md:flex items-center gap-1 absolute left-1/2 -translate-x-1/2">
|
||||
{navLinks.map(({ href, label }) => (
|
||||
<a
|
||||
key={href}
|
||||
href={href}
|
||||
className="rounded-md px-2 py-1.5 text-sm font-medium text-black/75 hover:text-black hover:bg-black/10 transition-colors"
|
||||
>
|
||||
{label}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<nav className="flex items-center gap-2 md:gap-3">
|
||||
<AnimatedThemeToggler className={navbarActionClassName} />
|
||||
{!isPublicDomain &&
|
||||
(session?.user ? (
|
||||
<Link prefetch href="/dashboard" className="hidden md:block">
|
||||
<nav className="ml-auto flex items-center gap-2 md:gap-3">
|
||||
<AnimatedThemeToggler variant="navbar" />
|
||||
{!isPublicDomain &&
|
||||
(session?.user ? (
|
||||
<Link prefetch href="/dashboard" className="hidden md:block">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="border-black/20 text-black/80 bg-transparent hover:bg-black/10 hover:text-black shadow-none"
|
||||
>
|
||||
Dashboard
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<Link href="/login">
|
||||
<Button
|
||||
variant="outline"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="border-black/20 text-black/80 bg-transparent hover:bg-black/10 hover:text-black shadow-none"
|
||||
className="text-black/75 hover:bg-black/10 hover:text-black shadow-none"
|
||||
>
|
||||
Dashboard
|
||||
Entrar
|
||||
</Button>
|
||||
</Link>
|
||||
) : (
|
||||
<div className="hidden md:flex items-center gap-2">
|
||||
<Link href="/login">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-black/75 hover:bg-black/10 hover:text-black shadow-none"
|
||||
>
|
||||
Entrar
|
||||
</Button>
|
||||
</Link>
|
||||
<Link href="/signup">
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-black/10 border border-black/20 text-black shadow-none hover:bg-black/20 gap-2"
|
||||
>
|
||||
Começar
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
<MobileNav
|
||||
isPublicDomain={isPublicDomain}
|
||||
isLoggedIn={!!session?.user}
|
||||
triggerClassName="border border-black/10 text-black/75 shadow-none hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-black/20"
|
||||
/>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
<Link href="/signup">
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-black/10 border border-black/20 text-black shadow-none hover:bg-black/20 gap-2"
|
||||
>
|
||||
Começar
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
))}
|
||||
<MobileNav
|
||||
isPublicDomain={isPublicDomain}
|
||||
isLoggedIn={!!session?.user}
|
||||
/>
|
||||
</nav>
|
||||
</NavbarShell>
|
||||
|
||||
{/* Hero Section */}
|
||||
<section className="relative overflow-hidden pt-14 md:pt-20 lg:pt-24 pb-0">
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Analytics } from "@vercel/analytics/next";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/next";
|
||||
import type { Metadata } from "next";
|
||||
import { ThemeProvider } from "@/shared/components/providers/theme-provider";
|
||||
import { Toaster } from "@/shared/components/ui/sonner";
|
||||
@@ -28,14 +26,18 @@ export default function RootLayout({
|
||||
>
|
||||
<head>
|
||||
<meta name="apple-mobile-web-app-title" content="OpenMonetis" />
|
||||
<script
|
||||
defer
|
||||
src="https://umami.felipecoutinho.com/script.js"
|
||||
data-website-id="ea438854-a014-42ea-b416-0a8321471f0f"
|
||||
data-domains="openmonetis.com"
|
||||
/>
|
||||
</head>
|
||||
<body className="subpixel-antialiased" suppressHydrationWarning>
|
||||
<ThemeProvider attribute="class" defaultTheme="light">
|
||||
{children}
|
||||
<Toaster position="top-right" />
|
||||
</ThemeProvider>
|
||||
<Analytics />
|
||||
<SpeedInsights />
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -125,7 +125,7 @@ export function LoginForm({ className, ...props }: DivProps) {
|
||||
});
|
||||
|
||||
if (passkeyError) {
|
||||
setError(passkeyError.message || "Erro ao entrar com passkey.");
|
||||
setError((passkeyError.message as string) || "Erro ao entrar com passkey.");
|
||||
setLoadingPasskey(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,24 +24,18 @@ const navLinks = [
|
||||
interface MobileNavProps {
|
||||
isPublicDomain: boolean;
|
||||
isLoggedIn: boolean;
|
||||
triggerClassName?: string;
|
||||
}
|
||||
|
||||
export function MobileNav({
|
||||
isPublicDomain,
|
||||
isLoggedIn,
|
||||
triggerClassName,
|
||||
}: MobileNavProps) {
|
||||
export function MobileNav({ isPublicDomain, isLoggedIn }: MobileNavProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<div className="md:hidden">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
variant="navbar"
|
||||
size="icon-sm"
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label="Abrir menu"
|
||||
className={triggerClassName}
|
||||
>
|
||||
<RiMenuLine className="size-5" />
|
||||
</Button>
|
||||
|
||||
@@ -35,9 +35,6 @@ export type FeatureItem = {
|
||||
colorVar: string;
|
||||
};
|
||||
|
||||
export const navbarActionClassName =
|
||||
"border-black/10 bg-transparent text-black/75 shadow-none hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-black/20 data-[state=open]:bg-black/10 data-[state=open]:text-black";
|
||||
|
||||
export const navLinks = [
|
||||
{ href: "#telas", label: "conheça as telas" },
|
||||
{ href: "#funcionalidades", label: "funcionalidades" },
|
||||
|
||||
@@ -73,7 +73,7 @@ export function PasskeysForm() {
|
||||
const { data, error: fetchError } =
|
||||
await authClient.passkey.listUserPasskeys();
|
||||
if (fetchError) {
|
||||
setError(fetchError.message || "Erro ao carregar passkeys.");
|
||||
setError((fetchError.message as string) || "Erro ao carregar passkeys.");
|
||||
return;
|
||||
}
|
||||
setPasskeys(
|
||||
@@ -111,7 +111,7 @@ export function PasskeysForm() {
|
||||
name: addName.trim() || undefined,
|
||||
});
|
||||
if (addError) {
|
||||
setError(addError.message || "Erro ao registrar passkey.");
|
||||
setError((addError.message as string) || "Erro ao registrar passkey.");
|
||||
return;
|
||||
}
|
||||
setAddName("");
|
||||
@@ -134,7 +134,7 @@ export function PasskeysForm() {
|
||||
name: editName.trim(),
|
||||
});
|
||||
if (renameError) {
|
||||
setError(renameError.message || "Erro ao renomear passkey.");
|
||||
setError((renameError.message as string) || "Erro ao renomear passkey.");
|
||||
return;
|
||||
}
|
||||
setEditingId(null);
|
||||
@@ -156,7 +156,7 @@ export function PasskeysForm() {
|
||||
id: deleteId,
|
||||
});
|
||||
if (deleteError) {
|
||||
setError(deleteError.message || "Erro ao remover passkey.");
|
||||
setError((deleteError.message as string) || "Erro ao remover passkey.");
|
||||
return;
|
||||
}
|
||||
setDeleteId(null);
|
||||
|
||||
@@ -116,8 +116,9 @@ export function TransactionDialog({
|
||||
},
|
||||
);
|
||||
|
||||
// Derive credit card period on open when cardId is pre-filled
|
||||
// Derive credit card period on open when cardId is pre-filled (create only)
|
||||
if (
|
||||
mode !== "update" &&
|
||||
initial.paymentMethod === "Cartão de crédito" &&
|
||||
initial.cardId &&
|
||||
initial.purchaseDate
|
||||
|
||||
1
src/global.d.ts
vendored
Normal file
1
src/global.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
declare module "*.css";
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
import { RiMoonClearLine, RiSunLine } from "@remixicon/react";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { flushSync } from "react-dom";
|
||||
import { buttonVariants } from "@/shared/components/ui/button";
|
||||
@@ -13,11 +14,13 @@ import { cn } from "@/shared/utils/ui";
|
||||
interface AnimatedThemeTogglerProps
|
||||
extends React.ComponentPropsWithoutRef<"button"> {
|
||||
duration?: number;
|
||||
variant?: VariantProps<typeof buttonVariants>["variant"];
|
||||
}
|
||||
|
||||
export const AnimatedThemeToggler = ({
|
||||
className,
|
||||
duration = 400,
|
||||
variant = "ghost",
|
||||
...props
|
||||
}: AnimatedThemeTogglerProps) => {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
@@ -84,10 +87,10 @@ export const AnimatedThemeToggler = ({
|
||||
onClick={toggleTheme}
|
||||
data-state={isDark ? "dark" : "light"}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost", size: "icon-sm" }),
|
||||
"group relative text-muted-foreground transition-all duration-200",
|
||||
"hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40",
|
||||
"data-[state=open]:bg-accent/60 data-[state=open]:text-foreground",
|
||||
buttonVariants({ variant, size: "icon-sm" }),
|
||||
"group relative transition-all duration-200",
|
||||
variant === "ghost" &&
|
||||
"text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40 data-[state=open]:bg-accent/60 data-[state=open]:text-foreground",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import Image from "next/image";
|
||||
import { version } from "@/package.json";
|
||||
import { cn } from "@/shared/utils/ui";
|
||||
|
||||
interface LogoProps {
|
||||
variant?: "full" | "small" | "compact";
|
||||
className?: string;
|
||||
showVersion?: boolean;
|
||||
/** Apenas nos variants "full" e "compact" */
|
||||
invertTextOnDark?: boolean;
|
||||
/** Exibe o ícone na cor original, sem filtro preto */
|
||||
/** Exibe o ícone na cor original, sem filtro preto. Apenas nos variants "full" e "compact" */
|
||||
colorIcon?: boolean;
|
||||
}
|
||||
|
||||
const iconFilterClass = "brightness-0 saturate-0";
|
||||
|
||||
export function Logo({
|
||||
variant = "full",
|
||||
className,
|
||||
showVersion = false,
|
||||
invertTextOnDark = true,
|
||||
colorIcon = false,
|
||||
}: LogoProps) {
|
||||
@@ -26,10 +26,7 @@ export function Logo({
|
||||
alt="OpenMonetis"
|
||||
width={32}
|
||||
height={32}
|
||||
className={cn(
|
||||
"object-contain",
|
||||
!colorIcon && "brightness-0 saturate-0",
|
||||
)}
|
||||
className={cn("object-contain", !colorIcon && iconFilterClass)}
|
||||
priority
|
||||
/>
|
||||
<Image
|
||||
@@ -67,7 +64,7 @@ export function Logo({
|
||||
alt="OpenMonetis"
|
||||
width={28}
|
||||
height={28}
|
||||
className="object-contain"
|
||||
className={cn("object-contain", !colorIcon && iconFilterClass)}
|
||||
priority
|
||||
/>
|
||||
<Image
|
||||
@@ -78,11 +75,6 @@ export function Logo({
|
||||
className={cn("object-contain", invertTextOnDark && "dark:invert")}
|
||||
priority
|
||||
/>
|
||||
{showVersion && (
|
||||
<span className="text-[9px] font-medium text-muted-foreground">
|
||||
{version}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import Link from "next/link";
|
||||
import { AnimatedThemeToggler } from "@/shared/components/animated-theme-toggler";
|
||||
import { Logo } from "@/shared/components/logo";
|
||||
import { NotificationBell } from "@/shared/components/navigation/navbar/notification-bell";
|
||||
import { RefreshPageButton } from "@/shared/components/refresh-page-button";
|
||||
import type { DashboardNotificationsSnapshot } from "@/shared/lib/types/notifications";
|
||||
import { NavMenu } from "./nav-menu";
|
||||
import { NavbarShell } from "./navbar-shell";
|
||||
import { NavbarUser } from "./navbar-user";
|
||||
|
||||
type AppNavbarProps = {
|
||||
@@ -19,9 +18,6 @@ type AppNavbarProps = {
|
||||
notificationsSnapshot: DashboardNotificationsSnapshot;
|
||||
};
|
||||
|
||||
const navbarActionClassName =
|
||||
"border-black/10 bg-transparent text-black/75 shadow-none hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-black/20 data-[state=open]:bg-black/10 data-[state=open]:text-black";
|
||||
|
||||
export function AppNavbar({
|
||||
user,
|
||||
pagadorAvatarUrl,
|
||||
@@ -29,28 +25,20 @@ export function AppNavbar({
|
||||
notificationsSnapshot,
|
||||
}: AppNavbarProps) {
|
||||
return (
|
||||
<header className="fixed top-0 left-0 right-0 z-50 flex h-16 shrink-0 items-center bg-primary">
|
||||
<div className="relative z-10 mx-auto flex h-full w-full max-w-8xl items-center gap-4 px-4">
|
||||
<Link href="/dashboard" className="shrink-0 mr-1">
|
||||
<Logo variant="compact" invertTextOnDark={false} />
|
||||
</Link>
|
||||
|
||||
<NavMenu />
|
||||
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<NotificationBell
|
||||
notifications={notificationsSnapshot.notifications}
|
||||
unreadCount={notificationsSnapshot.unreadCount}
|
||||
visibleCount={notificationsSnapshot.visibleCount}
|
||||
budgetNotifications={notificationsSnapshot.budgetNotifications}
|
||||
preLancamentosCount={preLancamentosCount}
|
||||
/>
|
||||
<RefreshPageButton className={navbarActionClassName} />
|
||||
<AnimatedThemeToggler className={navbarActionClassName} />
|
||||
</div>
|
||||
|
||||
<NavbarUser user={user} pagadorAvatarUrl={pagadorAvatarUrl} />
|
||||
<NavbarShell logoHref="/dashboard" fixed>
|
||||
<NavMenu />
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<NotificationBell
|
||||
notifications={notificationsSnapshot.notifications}
|
||||
unreadCount={notificationsSnapshot.unreadCount}
|
||||
visibleCount={notificationsSnapshot.visibleCount}
|
||||
budgetNotifications={notificationsSnapshot.budgetNotifications}
|
||||
preLancamentosCount={preLancamentosCount}
|
||||
/>
|
||||
<RefreshPageButton variant="navbar" />
|
||||
<AnimatedThemeToggler variant="navbar" />
|
||||
</div>
|
||||
</header>
|
||||
<NavbarUser user={user} pagadorAvatarUrl={pagadorAvatarUrl} />
|
||||
</NavbarShell>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,13 +20,18 @@ import {
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/shared/components/ui/sheet";
|
||||
import { cn } from "@/shared/utils/ui";
|
||||
import { MobileLink, MobileSectionLabel } from "./mobile-link";
|
||||
import { NavDropdown } from "./nav-dropdown";
|
||||
import { NAV_SECTIONS } from "./nav-items";
|
||||
import { NavPill } from "./nav-pill";
|
||||
import { triggerActiveClass, triggerClass } from "./nav-styles";
|
||||
import { MobileTools, NavToolsDropdown } from "./nav-tools";
|
||||
|
||||
const triggerClass =
|
||||
"h-8! rounded-md! px-2! py-0! text-sm! font-medium! bg-transparent! shadow-none! lowercase! [&_svg]:text-current! text-black/75! hover:text-black! hover:bg-black/10! focus:text-black! focus:bg-black/10! focus-visible:ring-black/20! data-[state=open]:text-black! data-[state=open]:bg-black/10!";
|
||||
|
||||
const triggerActiveClass = "bg-black/15! text-black!";
|
||||
|
||||
export function NavMenu() {
|
||||
const pathname = usePathname();
|
||||
const [sheetOpen, setSheetOpen] = useState(false);
|
||||
@@ -55,7 +60,10 @@ export function NavMenu() {
|
||||
return (
|
||||
<NavigationMenuItem key={section.label}>
|
||||
<NavigationMenuTrigger
|
||||
className={`${triggerClass} ${isSectionActive ? triggerActiveClass : ""}`}
|
||||
className={cn(
|
||||
triggerClass,
|
||||
isSectionActive && triggerActiveClass,
|
||||
)}
|
||||
>
|
||||
{section.label}
|
||||
</NavigationMenuTrigger>
|
||||
@@ -82,9 +90,9 @@ export function NavMenu() {
|
||||
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="-order-1 border border-black/10 text-black/75 shadow-none md:hidden hover:border-black/20 hover:bg-black/10 hover:text-black focus-visible:ring-black/20"
|
||||
variant="navbar"
|
||||
size="icon-sm"
|
||||
className="-order-1 md:hidden"
|
||||
>
|
||||
<RiMenuLine className="size-5" />
|
||||
<span className="sr-only">Abrir menu</span>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { usePathname } from "next/navigation";
|
||||
import { buttonVariants } from "@/shared/components/ui/button";
|
||||
import { cn } from "@/shared/utils/ui";
|
||||
import { NavLink } from "./nav-link";
|
||||
import { linkActive, linkBase, linkIdle } from "./nav-styles";
|
||||
|
||||
type NavPillProps = {
|
||||
href: string;
|
||||
@@ -23,7 +23,11 @@ export function NavPill({ href, preservePeriod, children }: NavPillProps) {
|
||||
<NavLink
|
||||
href={href}
|
||||
preservePeriod={preservePeriod}
|
||||
className={cn(linkBase, isActive ? linkActive : linkIdle)}
|
||||
className={cn(
|
||||
buttonVariants({ variant: "navbar", size: "sm" }),
|
||||
"lowercase",
|
||||
isActive && "bg-black/15 text-black",
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</NavLink>
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
export const linkBase =
|
||||
"inline-flex h-8 items-center justify-center rounded-md px-2 text-sm font-medium transition-all lowercase";
|
||||
|
||||
export const linkIdle = "text-black/75 hover:bg-black/10 hover:text-black";
|
||||
|
||||
export const linkActive = "bg-black/15 text-black";
|
||||
|
||||
export const triggerActiveClass = ["bg-black/15!", "text-black!"].join(" ");
|
||||
|
||||
export const triggerClass = [
|
||||
"h-8!",
|
||||
"rounded-md!",
|
||||
"px-2!",
|
||||
"py-0!",
|
||||
"text-sm!",
|
||||
"font-medium!",
|
||||
"bg-transparent!",
|
||||
"text-black/75!",
|
||||
"hover:text-black!",
|
||||
"hover:bg-black/10!",
|
||||
"focus:text-black!",
|
||||
"focus:bg-black/10!",
|
||||
"focus-visible:ring-black/20!",
|
||||
"data-[state=open]:text-black!",
|
||||
"data-[state=open]:bg-black/10!",
|
||||
"shadow-none!",
|
||||
"[&_svg]:text-current!",
|
||||
"lowercase!",
|
||||
].join(" ");
|
||||
33
src/shared/components/navigation/navbar/navbar-shell.tsx
Normal file
33
src/shared/components/navigation/navbar/navbar-shell.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import Link from "next/link";
|
||||
import { Logo } from "@/shared/components/logo";
|
||||
|
||||
type NavbarShellProps = {
|
||||
logoHref?: string;
|
||||
fixed?: boolean;
|
||||
children: React.ReactNode;
|
||||
};
|
||||
|
||||
export function NavbarShell({
|
||||
logoHref,
|
||||
fixed = false,
|
||||
children,
|
||||
}: NavbarShellProps) {
|
||||
const positionClass = fixed ? "fixed top-0 left-0 right-0" : "sticky top-0";
|
||||
|
||||
return (
|
||||
<header
|
||||
className={`${positionClass} z-50 flex h-16 shrink-0 items-center bg-primary`}
|
||||
>
|
||||
<div className="relative z-10 mx-auto flex h-full w-full max-w-8xl items-center gap-4 px-4">
|
||||
{logoHref ? (
|
||||
<Link href={logoHref} className="shrink-0">
|
||||
<Logo variant="compact" invertTextOnDark={false} />
|
||||
</Link>
|
||||
) : (
|
||||
<Logo variant="compact" invertTextOnDark={false} />
|
||||
)}
|
||||
{children}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
@@ -78,6 +78,6 @@ function LogoContent() {
|
||||
const isCollapsed = state === "collapsed";
|
||||
|
||||
return (
|
||||
<Logo variant={isCollapsed ? "small" : "full"} showVersion={!isCollapsed} />
|
||||
<Logo variant={isCollapsed ? "small" : "full"} />
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { RiRefreshLine } from "@remixicon/react";
|
||||
import type { VariantProps } from "class-variance-authority";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useTransition } from "react";
|
||||
import { buttonVariants } from "@/shared/components/ui/button";
|
||||
@@ -11,10 +12,12 @@ import {
|
||||
} from "@/shared/components/ui/tooltip";
|
||||
import { cn } from "@/shared/utils/ui";
|
||||
|
||||
type RefreshPageButtonProps = React.ComponentPropsWithoutRef<"button">;
|
||||
type RefreshPageButtonProps = React.ComponentPropsWithoutRef<"button"> &
|
||||
Pick<VariantProps<typeof buttonVariants>, "variant">;
|
||||
|
||||
export function RefreshPageButton({
|
||||
className,
|
||||
variant = "ghost",
|
||||
...props
|
||||
}: RefreshPageButtonProps) {
|
||||
const router = useRouter();
|
||||
@@ -36,10 +39,10 @@ export function RefreshPageButton({
|
||||
aria-label="Atualizar página"
|
||||
title="Atualizar página"
|
||||
className={cn(
|
||||
buttonVariants({ variant: "ghost", size: "icon-sm" }),
|
||||
"size-8 text-muted-foreground transition-all duration-200",
|
||||
"hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40",
|
||||
"disabled:pointer-events-none disabled:opacity-50",
|
||||
buttonVariants({ variant, size: "icon-sm" }),
|
||||
"transition-all duration-200",
|
||||
variant === "ghost" &&
|
||||
"text-muted-foreground hover:text-foreground focus-visible:ring-2 focus-visible:ring-primary/40",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -19,6 +19,8 @@ const buttonVariants = cva(
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
navbar:
|
||||
"bg-transparent text-black/75 shadow-none hover:bg-black/10 hover:text-black focus-visible:ring-black/20",
|
||||
},
|
||||
size: {
|
||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"ignoreDeprecations": "6.0",
|
||||
"baseUrl": ".",
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"lib": [
|
||||
"dom",
|
||||
"dom.iterable",
|
||||
"esnext"
|
||||
],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
@@ -20,7 +25,10 @@
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*", "./*"]
|
||||
"@/*": [
|
||||
"./src/*",
|
||||
"./*"
|
||||
]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
@@ -34,5 +42,7 @@
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
"exclude": [
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user