Compare commits

..

3 Commits

Author SHA1 Message Date
f64fc274a7 Adds Playwright E2E and Vitest test infrastructure
Some checks failed
Lint / Lint and Check (push) Failing after 48s
Integrates Playwright for end-to-end browser testing with automated web server setup, example smoke tests, and CI-compatible configuration. Introduces Vitest, Testing Library, and related utilities for fast component and unit testing.

Updates scripts, development dependencies, and lockfile to support both test suites. Establishes unified testing commands for local and CI workflows, laying groundwork for comprehensive automated UI and integration coverage.
2025-11-24 22:43:46 +01:00
65f1fcb7bb Adds habit edit, archive, and undo log features
Enables users to update or archive habits and to undo the latest habit log.
Adds PATCH/DELETE API endpoints for habit edit and soft deletion.
Introduces UI dialogs and controls for editing and archiving habits,
as well as for undoing the most recent log entry directly from the dashboard.
Improves log statistics handling by ordering and simplifies last log detection.
2025-11-24 22:12:25 +01:00
55950e9473 Plan 2025-11-24 22:02:40 +01:00
21 changed files with 956 additions and 1110 deletions

View File

@@ -1,7 +0,0 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
.git

View File

@@ -13,13 +13,13 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4
- name: Setup Node.js
uses: actions/setup-node@395ad3262231945c25e8478fd5baf05154b1d79f # v6
uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6
with:
node-version: 24
cache: 'pnpm'

View File

@@ -1,69 +0,0 @@
# syntax=docker.io/docker/dockerfile:1@sha256:b6afd42430b15f2d2a4c5a02b919e98a525b785b1aaff16747d2f623364e39b6
FROM node:24-alpine@sha256:7e0bd0460b26eb3854ea5b99b887a6a14d665d14cae694b78ae2936d14b2befb AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED=1
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
# Production image, copy all the files and run next
FROM base AS runner
# wget needed for healthcheck
RUN apk add --no-cache wget
WORKDIR /app
ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy public files. "[c]" as workaround for conditional matching since the folder might not exist.
COPY --from=builder /app/publi[c] ./public
# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/config/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.6 KiB

View File

@@ -12,61 +12,53 @@ vi.mock('next/navigation', () => ({
// Mock ResizeObserver
global.ResizeObserver = class ResizeObserver {
observe(): void {
/* noop */
}
unobserve(): void {
/* noop */
}
disconnect(): void {
/* noop */
}
observe() {}
unobserve() {}
disconnect() {}
};
// Mock fetch
global.fetch = vi.fn();
const createTestQueryClient = () =>
new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
const createTestQueryClient = () => new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
});
},
});
describe('Dashboard', () => {
beforeEach(() => {
vi.clearAllMocks();
// Mock Auth Response
vi.mocked(global.fetch).mockImplementation((url: string | URL | Request) => {
(global.fetch as any).mockImplementation((url: string) => {
if (url === '/api/auth') {
return Promise.resolve({
json: () => Promise.resolve({ authenticated: true, token: 'test-token' }),
} as Response);
});
}
if (url === '/api/habits') {
return Promise.resolve({
ok: true,
json: () =>
Promise.resolve({
habits: [
{
id: 1,
name: 'Test Habit',
type: 'neutral',
totalLogs: 5,
logsLastWeek: 2,
logsLastMonth: 5,
createdAt: new Date().toISOString(),
lastLoggedAt: new Date().toISOString(),
},
],
}),
} as Response);
json: () => Promise.resolve({
habits: [
{
id: 1,
name: 'Test Habit',
type: 'neutral',
totalLogs: 5,
logsLastWeek: 2,
logsLastMonth: 5,
createdAt: new Date().toISOString(),
lastLoggedAt: new Date().toISOString(),
}
]
}),
});
}
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) } as Response);
return Promise.resolve({ ok: true, json: () => Promise.resolve({}) });
});
});
@@ -74,7 +66,7 @@ describe('Dashboard', () => {
render(
<QueryClientProvider client={createTestQueryClient()}>
<Dashboard />
</QueryClientProvider>,
</QueryClientProvider>
);
await waitFor(() => {
@@ -86,7 +78,7 @@ describe('Dashboard', () => {
render(
<QueryClientProvider client={createTestQueryClient()}>
<Dashboard />
</QueryClientProvider>,
</QueryClientProvider>
);
await waitFor(() => {
@@ -107,7 +99,7 @@ describe('Dashboard', () => {
render(
<QueryClientProvider client={createTestQueryClient()}>
<Dashboard />
</QueryClientProvider>,
</QueryClientProvider>
);
await waitFor(() => {

View File

@@ -77,12 +77,12 @@ interface HabitResponse {
export default function Dashboard() {
const router = useRouter();
const queryClient = useQueryClient();
// State
const [showNewHabitDialog, setShowNewHabitDialog] = useState(false);
const [newHabitName, setNewHabitName] = useState('');
const [newHabitType, setNewHabitType] = useState<'positive' | 'neutral' | 'negative'>('neutral');
const [editingHabit, setEditingHabit] = useState<Habit | null>(null);
const [editHabitName, setEditHabitName] = useState('');
const [editHabitType, setEditHabitType] = useState<'positive' | 'neutral' | 'negative'>('neutral');
@@ -109,9 +109,7 @@ export default function Dashboard() {
const interval = setInterval(() => {
setCurrentTime(Date.now());
}, 60000);
return () => {
clearInterval(interval);
};
return () => clearInterval(interval);
}, []);
// Fetch habits
@@ -142,9 +140,9 @@ export default function Dashboard() {
});
// Undo log mutation
const undoLogMutation = useMutation<unknown, Error, number>({
const undoLogMutation = useMutation<void, Error, number>({
mutationFn: async (habitId: number): Promise<void> => {
const res = await fetch(`/api/habits/${String(habitId)}/log`, {
const res = await fetch(`/api/habits/${habitId}/log`, {
method: 'DELETE',
});
if (!res.ok) throw new Error('Failed to undo log');
@@ -174,13 +172,9 @@ export default function Dashboard() {
});
// Update habit mutation
const updateHabitMutation = useMutation<
HabitResponse,
Error,
{ id: number; name: string; type: string }
>({
const updateHabitMutation = useMutation<HabitResponse, Error, { id: number; name: string; type: string }>({
mutationFn: async ({ id, name, type }): Promise<HabitResponse> => {
const res = await fetch(`/api/habits/${String(id)}`, {
const res = await fetch(`/api/habits/${id}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, type }),
@@ -195,9 +189,9 @@ export default function Dashboard() {
});
// Delete habit mutation
const deleteHabitMutation = useMutation<unknown, Error, number>({
const deleteHabitMutation = useMutation<void, Error, number>({
mutationFn: async (id: number): Promise<void> => {
const res = await fetch(`/api/habits/${String(id)}`, {
const res = await fetch(`/api/habits/${id}`, {
method: 'DELETE',
});
if (!res.ok) throw new Error('Failed to delete habit');
@@ -456,10 +450,8 @@ export default function Dashboard() {
variant="ghost"
size="icon"
className="h-8 w-8 opacity-0 transition-opacity group-hover:opacity-100 hover:bg-black/20"
onClick={(e) => {
openEditDialog(e, habit);
}}
data-testid={`edit-habit-${String(habit.id)}`}
onClick={(e) => openEditDialog(e, habit)}
data-testid={`edit-habit-${habit.id}`}
>
<MoreVertical className="h-4 w-4" />
</Button>
@@ -471,34 +463,34 @@ export default function Dashboard() {
{/* Last logged */}
<div className="flex items-center justify-between text-sm">
<div className="flex items-center gap-2">
<Clock className="h-4 w-4 text-zinc-500" />
<span className="text-zinc-400">
<Clock className="h-4 w-4 text-zinc-500" />
<span className="text-zinc-400">
{habit.lastLoggedAt
? formatDistanceToNow(new Date(habit.lastLoggedAt), { addSuffix: true })
: 'Never logged'}
</span>
? formatDistanceToNow(new Date(habit.lastLoggedAt), { addSuffix: true })
: 'Never logged'}
</span>
</div>
{habit.lastLoggedAt && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-zinc-500 hover:bg-red-950/30 hover:text-red-400"
onClick={(e) => {
e.stopPropagation();
undoLogMutation.mutate(habit.id);
}}
>
<RotateCcw className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Undo last log</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-6 w-6 text-zinc-500 hover:text-red-400 hover:bg-red-950/30"
onClick={(e) => {
e.stopPropagation();
undoLogMutation.mutate(habit.id);
}}
>
<RotateCcw className="h-3 w-3" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Undo last log</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
@@ -561,14 +553,9 @@ export default function Dashboard() {
</Card>
)}
</div>
{/* Edit Habit Dialog */}
<Dialog
open={!!editingHabit}
onOpenChange={(open) => {
if (!open) setEditingHabit(null);
}}
>
<Dialog open={!!editingHabit} onOpenChange={(open) => !open && setEditingHabit(null)}>
<DialogContent className="border-zinc-800 bg-zinc-950">
<DialogHeader>
<DialogTitle>Edit Habit</DialogTitle>
@@ -582,9 +569,7 @@ export default function Dashboard() {
<Input
id="edit-name"
value={editHabitName}
onChange={(e) => {
setEditHabitName(e.target.value);
}}
onChange={(e) => setEditHabitName(e.target.value)}
className="border-zinc-800 bg-zinc-900"
/>
</div>
@@ -623,56 +608,47 @@ export default function Dashboard() {
</div>
</div>
<DialogFooter className="flex-col items-stretch gap-2 sm:flex-row sm:justify-between">
<div className="flex flex-1 justify-start">
<div className="flex flex-1 justify-start">
{showDeleteConfirm ? (
<div className="flex items-center gap-2">
<Button
variant="destructive"
onClick={handleDeleteHabit}
disabled={deleteHabitMutation.isPending}
>
{deleteHabitMutation.isPending ? 'Deleting...' : 'Confirm Delete'}
</Button>
<Button
variant="ghost"
onClick={() => {
setShowDeleteConfirm(false);
}}
>
Cancel
</Button>
</div>
<div className="flex items-center gap-2">
<Button
variant="destructive"
onClick={handleDeleteHabit}
disabled={deleteHabitMutation.isPending}
>
{deleteHabitMutation.isPending ? 'Deleting...' : 'Confirm Delete'}
</Button>
<Button variant="ghost" onClick={() => setShowDeleteConfirm(false)}>
Cancel
</Button>
</div>
) : (
<Button
variant="outline"
className="border-red-900 text-red-500 hover:bg-red-950 hover:text-red-400"
onClick={() => {
setShowDeleteConfirm(true);
}}
onClick={() => setShowDeleteConfirm(true)}
>
<Trash2 className="mr-2 h-4 w-4" />
Archive Habit
</Button>
)}
</div>
<div className="flex items-center justify-end gap-2">
<Button
variant="outline"
onClick={() => {
setEditingHabit(null);
}}
>
Cancel
</Button>
<Button
onClick={handleUpdateHabit}
disabled={!editHabitName.trim() || updateHabitMutation.isPending}
className="bg-emerald-600 hover:bg-emerald-700"
>
<Save className="mr-2 h-4 w-4" />
{updateHabitMutation.isPending ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</div>
<div className="flex items-center gap-2 justify-end">
<Button
variant="outline"
onClick={() => setEditingHabit(null)}
>
Cancel
</Button>
<Button
onClick={handleUpdateHabit}
disabled={!editHabitName.trim() || updateHabitMutation.isPending}
className="bg-emerald-600 hover:bg-emerald-700"
>
<Save className="mr-2 h-4 w-4" />
{updateHabitMutation.isPending ? 'Saving...' : 'Save Changes'}
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View File

@@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="255.99998" height="255.99998"><svg width="255.99998" height="255.99998" viewBox="0 0 67.733328 67.733329" version="1.1" id="SvgjsSvg1501" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"><defs id="SvgjsDefs1500"></defs><g id="SvgjsG1499"><rect style="font-variation-settings:'wght' 800;display:none;fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4.23333;stroke-linecap:round;stroke-linejoin:round" id="SvgjsRect1498" width="67.73333" height="67.73333" x="0" y="0"></rect><circle style="font-variation-settings:'wght' 800;display:inline;fill:#059669;fill-opacity:1;stroke:none;stroke-width:5.27484;stroke-linecap:round;stroke-linejoin:round" id="SvgjsCircle1497" cy="12.297192" cx="45.428497" r="8.4252577"></circle><circle style="font-variation-settings:'wght' 800;display:inline;fill:#d97706;fill-opacity:1;stroke:none;stroke-width:5.27484;stroke-linecap:round;stroke-linejoin:round" id="SvgjsCircle1496" cy="55.690498" cx="20.649494" r="8.4252577"></circle><path d="M 41.252138,22.687586 C 37.704621,21.261737 35.129699,18.124539 34.422921,14.367094 27.263765,12.036843 21.1959,16.3091 21.1959,16.3091 26.828105,11.378756 34.25937,11.488211 34.25937,11.488211 34.4658,8.6374649 35.754453,5.9735431 37.861353,4.0421364 27.616916,4.6289911 20.55393,9.007259 20.55393,9.007259 c 0,0 7.215201,-6.4715959 18.21468,-7.3018404 C 27.909388,0.39887564 15.094082,4.9774108 6.3514003,18.315496 c 4.0103957,-4.912052 8.0242977,-5.536907 8.0242977,-5.536907 0,0 -8.0242977,5.536907 -11.6352968,22.306673 C 6.693033,24.761888 14.134879,20.722399 14.134879,20.722399 c 0,0 -11.3944778,14.362863 -6.4196968,29.448183 -2.2340419,-20.604045 17.5727098,-29.52867 17.5727098,-29.52867 0,0 -9.999614,7.430395 -9.869074,14.604325 6.849011,-12.913208 16.759463,-12.648153 16.759463,-12.648153 0,0 -1.654004,0.160095 -2.797748,3.339924 4.372381,-3.267307 9.297788,-3.366744 11.871607,-3.250413 z" style="font-variation-settings:'wght' 800;display:inline;fill:#10b981;stroke-width:5.27484;stroke-linecap:round;stroke-linejoin:round" id="SvgjsPath1495"></path><path d="M 58.307152,17.632961 C 57.576231,39.826641 38.88902,48.927208 38.88902,48.927208 c 0,0 8.797937,-6.66472 11.674571,-16.28871 -9.182522,13.25164 -17.211482,12.958514 -17.211482,12.958514 0,0 1.23553,-0.13002 3.330258,-3.771331 0,0 -3.711354,3.633872 -11.73317,3.573653 3.398948,1.419962 5.877586,4.426569 6.623169,8.033957 6.622795,1.558117 13.334552,-1.898221 13.334552,-1.898221 0,0 -6.384514,4.403344 -13.142669,5.086176 -0.229172,2.737305 -1.460403,5.293772 -3.45775,7.179499 8.51737,-0.09688 17.40272,-5.405551 17.40272,-5.405551 0,0 -5.901793,5.983606 -18.21468,7.743557 7.503974,0.399663 25.113814,-1.775961 32.296802,-16.610078 -1.747838,3.616163 -8.224552,5.57683 -8.224552,5.57683 0,0 8.224552,-5.57683 11.474321,-22.22683 -2.178474,6.160256 -11.070591,14.364798 -11.070591,14.364798 0,0 11.070591,-14.364798 6.336629,-29.610449 z" style="font-variation-settings:'wght' 800;display:inline;fill:#f59e0b;fill-opacity:1;stroke-width:5.27484;stroke-linecap:round;stroke-linejoin:round" id="SvgjsPath1494"></path></g></svg><style>@media (prefers-color-scheme: light) { :root { filter: none; } }
@media (prefers-color-scheme: dark) { :root { filter: none; } }
</style></svg>

Before

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.2 KiB

View File

@@ -28,7 +28,6 @@ export default function RootLayout({
return (
<html lang="en" className="scroll-smooth">
<head>
<meta name="apple-mobile-web-app-title" content="Track Every Day" />
<PlausibleProvider
domain="trackevery.day"
customDomain="https://analytics.schulze.network"

View File

@@ -1,21 +0,0 @@
{
"name": "Track Every Day",
"short_name": "Track",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -1,6 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
};
const nextConfig = {};
export default nextConfig;

View File

@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"scripts": {
"dev": "next dev",
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next typegen && eslint . && npx tsc --noEmit",
@@ -31,49 +31,49 @@
"clsx": "^2.1.1",
"cssnano": "^7.1.2",
"date-fns": "^4.1.0",
"drizzle-orm": "^0.45.0",
"lucide-react": "^0.561.0",
"drizzle-orm": "^0.44.7",
"lucide-react": "^0.554.0",
"nanoid": "^5.1.6",
"next": "16.0.10",
"next": "16.0.3",
"next-plausible": "^3.12.5",
"pg": "^8.16.3",
"pg-native": "^3.5.2",
"postcss-flexbugs-fixes": "^5.0.2",
"postcss-preset-env": "^10.4.0",
"react": "19.2.3",
"react-dom": "19.2.3",
"react": "19.2.0",
"react-dom": "19.2.0",
"tailwind-merge": "^3.4.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@playwright/test": "^1.57.0",
"@tailwindcss/postcss": "4.1.18",
"@playwright/test": "^1.56.1",
"@tailwindcss/postcss": "4.1.17",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.0",
"@types/node": "24.10.3",
"@types/pg": "8.16.0",
"@types/react": "19.2.7",
"@types/node": "24.10.1",
"@types/pg": "8.15.6",
"@types/react": "19.2.6",
"@types/react-dom": "19.2.3",
"@vitejs/plugin-react": "^5.1.2",
"drizzle-kit": "0.31.8",
"eslint": "9.39.2",
"eslint-config-next": "16.0.10",
"eslint-config-prettier": "10.1.8",
"jsdom": "^27.3.0",
"@vitejs/plugin-react": "^5.1.1",
"drizzle-kit": "0.31.7",
"eslint": "9.39.1",
"eslint-config-next": "16.0.3",
"eslint-config-prettier": "^10.1.8",
"jsdom": "^27.2.0",
"postcss": "8.5.6",
"prettier": "3.7.4",
"prettier-plugin-tailwindcss": "0.7.2",
"tailwindcss": "4.1.18",
"turbo": "2.6.3",
"prettier": "3.6.2",
"prettier-plugin-tailwindcss": "0.7.1",
"tailwindcss": "4.1.17",
"turbo": "2.6.1",
"typescript": "5.9.3",
"typescript-eslint": "8.49.0",
"vitest": "^4.0.15"
"typescript-eslint": "8.47.0",
"vitest": "^4.0.13"
},
"packageManager": "pnpm@10.25.0",
"packageManager": "pnpm@10.23.0",
"pnpm": {
"overrides": {
"@types/react": "19.2.7",
"@types/react": "19.2.6",
"@types/react-dom": "19.2.3"
}
}

1633
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,27 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
"postcss-flexbugs-fixes": {
"postcss-preset-env": {
autoprefixer: {
flexbox: "no-2009",
},
stage: 3,
features: {
"custom-properties": false,
},
},
"@fullhuman/postcss-purgecss": {
content: [
"./pages/**/*.{js,jsx,ts,tsx}",
"./components/**/*.{js,jsx,ts,tsx}",
],
defaultExtractor: (content) => content.match(/[\w-/:]+(?<!:)/g) || [],
safelist: ["html", "body"],
},
},
cssnano: {},
},
};

1
public/next.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

1
public/vercel.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

5
sonar-project.properties Normal file
View File

@@ -0,0 +1,5 @@
sonar.projectKey=trackevery.day
# relative paths to source directories. More details and properties are described
# at https://docs.sonarqube.org/latest/project-administration/narrowing-the-focus/
sonar.sources=.

12
turbo.json Normal file
View File

@@ -0,0 +1,12 @@
{
"$schema": "https://turbo.build/schema.json",
"tasks": {
"build": {
"outputs": [
".next/**",
"!.next/cache/**"
]
},
"type-check": {}
}
}