Setup initial project
This commit is contained in:
commit
db79af06f7
119 changed files with 20761 additions and 0 deletions
47
packages/web/eslint.config.js
Normal file
47
packages/web/eslint.config.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import tseslint from 'typescript-eslint';
|
||||
import reactPlugin from 'eslint-plugin-react';
|
||||
import reactHooksPlugin from 'eslint-plugin-react-hooks';
|
||||
import nextPlugin from '@next/eslint-plugin-next';
|
||||
import prettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||
|
||||
export default tseslint.config(
|
||||
{ ignores: ['.next/**', 'coverage/**'] },
|
||||
...tseslint.configs.recommended,
|
||||
// React flat config — uses JSX runtime transform (no need to import React)
|
||||
reactPlugin.configs.flat['jsx-runtime'],
|
||||
// React Hooks
|
||||
{
|
||||
plugins: { 'react-hooks': reactHooksPlugin },
|
||||
rules: reactHooksPlugin.configs.recommended.rules,
|
||||
},
|
||||
// Next.js
|
||||
{
|
||||
plugins: { '@next/next': nextPlugin },
|
||||
rules: {
|
||||
...nextPlugin.configs.recommended.rules,
|
||||
...nextPlugin.configs['core-web-vitals'].rules,
|
||||
},
|
||||
},
|
||||
{
|
||||
settings: {
|
||||
react: { version: 'detect' },
|
||||
},
|
||||
rules: {
|
||||
'@typescript-eslint/no-explicit-any': 'error',
|
||||
'@typescript-eslint/explicit-member-accessibility': ['error', { accessibility: 'explicit' }],
|
||||
'@typescript-eslint/no-unused-vars': [
|
||||
'error',
|
||||
{
|
||||
argsIgnorePattern: '^_',
|
||||
varsIgnorePattern: '^_',
|
||||
caughtErrorsIgnorePattern: '^_',
|
||||
},
|
||||
],
|
||||
'@typescript-eslint/consistent-type-imports': [
|
||||
'error',
|
||||
{ prefer: 'type-imports', fixStyle: 'inline-type-imports' },
|
||||
],
|
||||
},
|
||||
},
|
||||
prettierRecommended,
|
||||
);
|
||||
6
packages/web/next-env.d.ts
vendored
Normal file
6
packages/web/next-env.d.ts
vendored
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
7
packages/web/next.config.ts
Normal file
7
packages/web/next.config.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import type { NextConfig } from 'next';
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
transpilePackages: ['@meshitrack/shared'],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
34
packages/web/package.json
Normal file
34
packages/web/package.json
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "@meshitrack/web",
|
||||
"version": "0.0.1",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "next dev --port 3000 --turbopack",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint src",
|
||||
"lint-fix": "eslint src --fix",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "echo 'no tests yet'"
|
||||
},
|
||||
"dependencies": {
|
||||
"@meshitrack/shared": "*",
|
||||
"next": "^16.2.0",
|
||||
"next-auth": "^5.0.0-beta.30",
|
||||
"react": "^19.2.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"swr": "^2.3.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@next/eslint-plugin-next": "^16.2.1",
|
||||
"@tailwindcss/postcss": "^4.2.0",
|
||||
"@types/node": "^25.0.0",
|
||||
"@types/react": "^19.0.0",
|
||||
"@types/react-dom": "^19.0.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"tailwindcss": "^4.2.0",
|
||||
"typescript": "^6.0.0"
|
||||
}
|
||||
}
|
||||
5
packages/web/postcss.config.mjs
Normal file
5
packages/web/postcss.config.mjs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
export default {
|
||||
plugins: {
|
||||
'@tailwindcss/postcss': {},
|
||||
},
|
||||
};
|
||||
61
packages/web/src/app/(dashboard)/dashboard/page.tsx
Normal file
61
packages/web/src/app/(dashboard)/dashboard/page.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
import Link from 'next/link';
|
||||
|
||||
export default function DashboardPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Dashboard</h1>
|
||||
<div className="grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||
<DashboardCard
|
||||
title="Product Library"
|
||||
description="Manage your food products and nutrition data"
|
||||
href="/products"
|
||||
/>
|
||||
<DashboardCard
|
||||
title="Recipes"
|
||||
description="Create and manage recipes with auto-nutrition"
|
||||
href="/recipes"
|
||||
/>
|
||||
<DashboardCard
|
||||
title="Pantry"
|
||||
description="Track what's in your fridge and pantry"
|
||||
href="/pantry"
|
||||
/>
|
||||
<DashboardCard
|
||||
title="Meal Plans"
|
||||
description="Plan your weekly meals and hit nutrition targets"
|
||||
href="/meal-plans"
|
||||
/>
|
||||
<DashboardCard
|
||||
title="Shopping Lists"
|
||||
description="Create shopping lists and track prices"
|
||||
href="/shopping"
|
||||
/>
|
||||
<DashboardCard
|
||||
title="Settings"
|
||||
description="Manage household and account settings"
|
||||
href="/settings"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DashboardCard({
|
||||
title,
|
||||
description,
|
||||
href,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="rounded-xl border bg-white p-6 shadow-sm hover:shadow-md transition-shadow"
|
||||
>
|
||||
<h2 className="text-lg font-semibold">{title}</h2>
|
||||
<p className="mt-1 text-sm text-gray-500">{description}</p>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
14
packages/web/src/app/(dashboard)/layout.tsx
Normal file
14
packages/web/src/app/(dashboard)/layout.tsx
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { Sidebar } from '@/components/layout/Sidebar';
|
||||
import { TopBar } from '@/components/layout/TopBar';
|
||||
|
||||
export default function DashboardLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex h-screen">
|
||||
<Sidebar />
|
||||
<div className="flex flex-1 flex-col">
|
||||
<TopBar />
|
||||
<main className="flex-1 overflow-auto p-6">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
packages/web/src/app/(dashboard)/loading.tsx
Normal file
7
packages/web/src/app/(dashboard)/loading.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default function Loading() {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
32
packages/web/src/app/(dashboard)/settings/page.tsx
Normal file
32
packages/web/src/app/(dashboard)/settings/page.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
export default function SettingsPage() {
|
||||
return (
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold mb-4">Settings</h1>
|
||||
<div className="max-w-2xl space-y-6">
|
||||
<section className="rounded-xl border bg-white p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold mb-4">Household</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Household management will be available here. Create a household, invite members, or
|
||||
switch between households.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="rounded-xl border bg-white p-6 shadow-sm">
|
||||
<h2 className="text-lg font-semibold mb-4">Account</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
Account settings are managed through Keycloak. Click the button below to manage your
|
||||
profile.
|
||||
</p>
|
||||
<a
|
||||
href={`${process.env.NEXT_PUBLIC_KEYCLOAK_URL || 'http://localhost:8080'}/realms/${process.env.NEXT_PUBLIC_KEYCLOAK_REALM || 'meshitrack'}/account`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-4 inline-block rounded-lg border px-4 py-2 text-sm font-medium hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
Manage Keycloak Account →
|
||||
</a>
|
||||
</section>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
packages/web/src/app/api/auth/[...nextauth]/route.ts
Normal file
3
packages/web/src/app/api/auth/[...nextauth]/route.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { handlers } from '@/lib/auth';
|
||||
|
||||
export const { GET, POST } = handlers;
|
||||
15
packages/web/src/app/layout.tsx
Normal file
15
packages/web/src/app/layout.tsx
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
import type { Metadata } from 'next';
|
||||
import '@/styles/globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'MeshiTrack',
|
||||
description: 'Nutrition & Pantry Management Platform',
|
||||
};
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className="min-h-screen bg-gray-50">{children}</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
20
packages/web/src/app/login/page.tsx
Normal file
20
packages/web/src/app/login/page.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
'use client';
|
||||
|
||||
import { signIn } from 'next-auth/react';
|
||||
|
||||
export default function LoginPage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="w-full max-w-sm rounded-xl border bg-white p-8 shadow-sm">
|
||||
<h1 className="text-2xl font-bold text-center mb-2">MeshiTrack</h1>
|
||||
<p className="text-gray-500 text-center text-sm mb-6">Sign in to manage your kitchen</p>
|
||||
<button
|
||||
onClick={() => signIn('keycloak', { callbackUrl: '/dashboard' })}
|
||||
className="block w-full rounded-lg bg-primary-600 py-3 text-center text-white font-medium hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
Sign in with Keycloak
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
packages/web/src/app/page.tsx
Normal file
21
packages/web/src/app/page.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import Link from 'next/link';
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-4xl font-bold text-primary-700 mb-4">MeshiTrack</h1>
|
||||
<p className="text-gray-600 mb-8 max-w-md">
|
||||
Your household nutrition & pantry management platform. Track food, plan meals, reduce
|
||||
waste, save money.
|
||||
</p>
|
||||
<Link
|
||||
href="/login"
|
||||
className="inline-block rounded-lg bg-primary-600 px-6 py-3 text-white font-medium hover:bg-primary-700 transition-colors"
|
||||
>
|
||||
Sign In
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
37
packages/web/src/components/layout/Sidebar.tsx
Normal file
37
packages/web/src/components/layout/Sidebar.tsx
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import Link from 'next/link';
|
||||
|
||||
const navItems = [
|
||||
{ label: 'Dashboard', href: '/dashboard' },
|
||||
{ label: 'Products', href: '/products' },
|
||||
{ label: 'Recipes', href: '/recipes' },
|
||||
{ label: 'Pantry', href: '/pantry' },
|
||||
{ label: 'Meal Plans', href: '/meal-plans' },
|
||||
{ label: 'Shopping', href: '/shopping' },
|
||||
{ label: 'Settings', href: '/settings' },
|
||||
];
|
||||
|
||||
export function Sidebar() {
|
||||
return (
|
||||
<aside className="flex w-64 flex-col border-r bg-white">
|
||||
<div className="flex h-16 items-center border-b px-6">
|
||||
<Link href="/dashboard" className="text-xl font-bold text-primary-700">
|
||||
MeshiTrack
|
||||
</Link>
|
||||
</div>
|
||||
<nav className="flex-1 overflow-y-auto p-4">
|
||||
<ul className="space-y-1">
|
||||
{navItems.map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link
|
||||
href={item.href}
|
||||
className="flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium text-gray-700 hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
{item.label}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
28
packages/web/src/components/layout/TopBar.tsx
Normal file
28
packages/web/src/components/layout/TopBar.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
import { auth } from '@/lib/auth';
|
||||
|
||||
export async function TopBar() {
|
||||
const session = await auth();
|
||||
const name = session?.user?.name ?? 'Unknown';
|
||||
const initial = name.charAt(0).toUpperCase();
|
||||
const householdId = session?.householdIds?.[0] ?? null;
|
||||
|
||||
return (
|
||||
<header className="flex h-16 items-center justify-between border-b bg-white px-6">
|
||||
<div className="text-sm text-gray-500">
|
||||
{householdId ? (
|
||||
<span className="rounded-md border px-3 py-1 font-medium text-gray-700">
|
||||
{householdId}
|
||||
</span>
|
||||
) : (
|
||||
<span className="rounded-md border px-3 py-1 text-gray-400">No household</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-600">{name}</span>
|
||||
<div className="h-8 w-8 rounded-full bg-primary-200 flex items-center justify-center text-sm font-medium text-primary-800">
|
||||
{initial}
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
52
packages/web/src/lib/auth.ts
Normal file
52
packages/web/src/lib/auth.ts
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
import NextAuth from 'next-auth';
|
||||
import Keycloak from 'next-auth/providers/keycloak';
|
||||
|
||||
const keycloakInternalUrl = process.env.KEYCLOAK_URL!;
|
||||
const keycloakPublicUrl = process.env.NEXT_PUBLIC_KEYCLOAK_URL!;
|
||||
const realm = process.env.KEYCLOAK_REALM!;
|
||||
|
||||
const internalBase = `${keycloakInternalUrl}/realms/${realm}`;
|
||||
const publicBase = `${keycloakPublicUrl}/realms/${realm}`;
|
||||
|
||||
export const { handlers, signIn, signOut, auth } = NextAuth({
|
||||
providers: [
|
||||
Keycloak({
|
||||
clientId: process.env.KEYCLOAK_CLIENT_ID!,
|
||||
clientSecret: process.env.KEYCLOAK_CLIENT_SECRET!,
|
||||
// issuer must match KC_HOSTNAME_URL so iss claim validation passes
|
||||
issuer: publicBase,
|
||||
// auth.js calls oauth4webapi.discoveryRequest(issuer) which constructs the URL from
|
||||
// `issuer` directly — our `wellKnown` override is ignored in that code path.
|
||||
// Bypass discovery entirely by pre-supplying all endpoints:
|
||||
// - authorization uses the public URL (browser redirect)
|
||||
// - token/userinfo/jwks use the internal Docker hostname (server-side fetches)
|
||||
// These values match what KC_HOSTNAME_URL would return in the discovery document anyway.
|
||||
authorization: `${publicBase}/protocol/openid-connect/auth`,
|
||||
token: `${internalBase}/protocol/openid-connect/token`,
|
||||
userinfo: `${internalBase}/protocol/openid-connect/userinfo`,
|
||||
jwks_endpoint: `${internalBase}/protocol/openid-connect/certs`,
|
||||
}),
|
||||
],
|
||||
pages: {
|
||||
signIn: '/login',
|
||||
},
|
||||
callbacks: {
|
||||
async jwt({ token, account, profile }) {
|
||||
if (account) {
|
||||
token.accessToken = account.access_token;
|
||||
token.refreshToken = account.refresh_token;
|
||||
token.expiresAt = account.expires_at;
|
||||
// householdIds is injected into the ID token by the Keycloak protocol mapper
|
||||
token.householdIds = (profile as Record<string, unknown>)?.['householdIds'] as
|
||||
| string[]
|
||||
| undefined;
|
||||
}
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
session.accessToken = token.accessToken as string;
|
||||
session.householdIds = (token.householdIds as string[] | undefined) ?? [];
|
||||
return session;
|
||||
},
|
||||
},
|
||||
});
|
||||
6
packages/web/src/proxy.ts
Normal file
6
packages/web/src/proxy.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
export { auth as proxy } from '@/lib/auth';
|
||||
|
||||
export const config = {
|
||||
// Protect all routes except auth callbacks, the login page, and Next.js internals.
|
||||
matcher: ['/((?!api/auth|login|_next/static|_next/image|favicon.ico).*)'],
|
||||
};
|
||||
73
packages/web/src/services/api-client.ts
Normal file
73
packages/web/src/services/api-client.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
const BASE_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001/api/v1';
|
||||
|
||||
class ApiClient {
|
||||
private _accessToken: string | null = null;
|
||||
|
||||
public set accessToken(token: string) {
|
||||
this._accessToken = token;
|
||||
}
|
||||
|
||||
private getHeaders(): Record<string, string> {
|
||||
const headers: Record<string, string> = {
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
|
||||
if (this._accessToken) {
|
||||
headers['Authorization'] = `Bearer ${this._accessToken}`;
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
public async get<T>(url: string): Promise<T> {
|
||||
const res = await fetch(`${BASE_URL}${url}`, {
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||
throw new Error(error.message || `Request failed: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
public async post<T>(url: string, body?: unknown): Promise<T> {
|
||||
const res = await fetch(`${BASE_URL}${url}`, {
|
||||
method: 'POST',
|
||||
headers: this.getHeaders(),
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||
throw new Error(error.message || `Request failed: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
public async patch<T>(url: string, body: unknown): Promise<T> {
|
||||
const res = await fetch(`${BASE_URL}${url}`, {
|
||||
method: 'PATCH',
|
||||
headers: this.getHeaders(),
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||
throw new Error(error.message || `Request failed: ${res.status}`);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
public async delete<T = void>(url: string): Promise<T> {
|
||||
const res = await fetch(`${BASE_URL}${url}`, {
|
||||
method: 'DELETE',
|
||||
headers: this.getHeaders(),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ message: res.statusText }));
|
||||
throw new Error(error.message || `Request failed: ${res.status}`);
|
||||
}
|
||||
if (res.status === 204) return undefined as T;
|
||||
return res.json();
|
||||
}
|
||||
}
|
||||
|
||||
export const apiClient = new ApiClient();
|
||||
24
packages/web/src/styles/globals.css
Normal file
24
packages/web/src/styles/globals.css
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
@theme {
|
||||
--color-primary-50: #f0fdf4;
|
||||
--color-primary-100: #dcfce7;
|
||||
--color-primary-200: #bbf7d0;
|
||||
--color-primary-300: #86efac;
|
||||
--color-primary-400: #4ade80;
|
||||
--color-primary-500: #22c55e;
|
||||
--color-primary-600: #16a34a;
|
||||
--color-primary-700: #15803d;
|
||||
--color-primary-800: #166534;
|
||||
--color-primary-900: #14532d;
|
||||
}
|
||||
|
||||
:root {
|
||||
--foreground-rgb: 0, 0, 0;
|
||||
--background-rgb: 255, 255, 255;
|
||||
}
|
||||
|
||||
body {
|
||||
color: rgb(var(--foreground-rgb));
|
||||
background: rgb(var(--background-rgb));
|
||||
}
|
||||
25
packages/web/tsconfig.json
Normal file
25
packages/web/tsconfig.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"verbatimModuleSyntax": false,
|
||||
"jsx": "preserve",
|
||||
"noEmit": true,
|
||||
"incremental": true,
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"isolatedModules": true,
|
||||
"allowJs": true,
|
||||
"plugins": [{ "name": "next" }],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@meshitrack/shared": ["../shared/src"],
|
||||
"@meshitrack/shared/*": ["../shared/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue