feat: добавил веб клиент
This commit is contained in:
parent
61f70eed71
commit
f6a817d7bd
2
web/.dockerignore
Normal file
2
web/.dockerignore
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
23
web/Dockerfile
Normal file
23
web/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
FROM node:22-alpine AS build
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json* ./
|
||||||
|
RUN npm install
|
||||||
|
COPY . .
|
||||||
|
ARG API_INTERNAL_URL=http://api:8000
|
||||||
|
ENV API_INTERNAL_URL=$API_INTERNAL_URL
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
FROM node:22-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
NEXT_TELEMETRY_DISABLED=1 \
|
||||||
|
HOSTNAME=0.0.0.0 \
|
||||||
|
PORT=3000 \
|
||||||
|
API_INTERNAL_URL=http://api:8000
|
||||||
|
COPY --from=build --chown=node:node /app/public ./public
|
||||||
|
COPY --from=build --chown=node:node /app/.next/standalone ./
|
||||||
|
COPY --from=build --chown=node:node /app/.next/static ./.next/static
|
||||||
|
USER node
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "server.js"]
|
||||||
55
web/app/backend/[...path]/route.ts
Normal file
55
web/app/backend/[...path]/route.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import { NextRequest, NextResponse } from "next/server";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
const DROP = new Set(["host", "connection", "content-length", "transfer-encoding"]);
|
||||||
|
|
||||||
|
async function proxy(request: NextRequest, context: { params: Promise<{ path: string[] }> }) {
|
||||||
|
const base = process.env.API_INTERNAL_URL;
|
||||||
|
if (!base) {
|
||||||
|
return NextResponse.json({ detail: "API_INTERNAL_URL is not set" }, { status: 500 });
|
||||||
|
}
|
||||||
|
const { path } = await context.params;
|
||||||
|
const target = new URL(`${base.replace(/\/$/, "")}/${path.join("/")}`);
|
||||||
|
target.search = request.nextUrl.search;
|
||||||
|
|
||||||
|
const headers = new Headers();
|
||||||
|
request.headers.forEach((value, key) => {
|
||||||
|
if (!DROP.has(key.toLowerCase())) {
|
||||||
|
headers.set(key, value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const hasBody = request.method !== "GET" && request.method !== "HEAD";
|
||||||
|
const upstream = await fetch(target, {
|
||||||
|
method: request.method,
|
||||||
|
headers,
|
||||||
|
body: hasBody ? await request.arrayBuffer() : undefined,
|
||||||
|
cache: "no-store",
|
||||||
|
redirect: "manual",
|
||||||
|
});
|
||||||
|
|
||||||
|
const responseHeaders = new Headers();
|
||||||
|
upstream.headers.forEach((value, key) => {
|
||||||
|
const name = key.toLowerCase();
|
||||||
|
if (name === "content-encoding" || name === "content-length" || name === "transfer-encoding" || name === "set-cookie") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
responseHeaders.set(key, value);
|
||||||
|
});
|
||||||
|
const cookies = typeof upstream.headers.getSetCookie === "function" ? upstream.headers.getSetCookie() : [];
|
||||||
|
for (const cookie of cookies) {
|
||||||
|
responseHeaders.append("set-cookie", cookie);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new NextResponse(await upstream.arrayBuffer(), {
|
||||||
|
status: upstream.status,
|
||||||
|
headers: responseHeaders,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const GET = proxy;
|
||||||
|
export const POST = proxy;
|
||||||
|
export const PUT = proxy;
|
||||||
|
export const PATCH = proxy;
|
||||||
|
export const DELETE = proxy;
|
||||||
58
web/app/cart/page.tsx
Normal file
58
web/app/cart/page.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import type { Cart } from "@/lib/types";
|
||||||
|
|
||||||
|
export default function CartPage() {
|
||||||
|
const [cart, setCart] = useState<Cart | null>(null);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
async function load() {
|
||||||
|
const response = await fetch("/backend/cart", { credentials: "include", cache: "no-store" });
|
||||||
|
if (!response.ok) {
|
||||||
|
setError("Не удалось загрузить корзину");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setCart(await response.json());
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void load();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
async function remove(productId: number) {
|
||||||
|
const response = await fetch(`/backend/cart/items/${productId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "include",
|
||||||
|
});
|
||||||
|
if (response.ok) {
|
||||||
|
setCart(await response.json());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <p className="error">{error}</p>;
|
||||||
|
}
|
||||||
|
if (!cart) {
|
||||||
|
return <p>Загрузка…</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Корзина</h1>
|
||||||
|
{cart.items.length === 0 && <p>Пока пусто.</p>}
|
||||||
|
{cart.items.map((item) => (
|
||||||
|
<p key={item.product_id}>
|
||||||
|
{item.title} × {item.qty} = {item.line_total} ₽{" "}
|
||||||
|
<button type="button" onClick={() => void remove(item.product_id)}>
|
||||||
|
Убрать
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<p>Итого: {cart.total} ₽</p>
|
||||||
|
{cart.items.length > 0 && <Link href="/checkout">Оформить заказ</Link>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
41
web/app/checkout/page.tsx
Normal file
41
web/app/checkout/page.tsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useState, type FormEvent } from "react";
|
||||||
|
|
||||||
|
export default function CheckoutPage() {
|
||||||
|
const router = useRouter();
|
||||||
|
const [name, setName] = useState("");
|
||||||
|
const [phone, setPhone] = useState("");
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
async function submit(event: FormEvent) {
|
||||||
|
event.preventDefault();
|
||||||
|
setError("");
|
||||||
|
const response = await fetch("/backend/orders", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ customer_name: name, customer_phone: phone }),
|
||||||
|
});
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) {
|
||||||
|
setError(typeof data.detail === "string" ? data.detail : "Проверьте имя и телефон");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
router.push(`/orders/${data.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Заказ</h1>
|
||||||
|
<p className="muted">Оплата не списывается. Заказ только записывается в базу.</p>
|
||||||
|
<form onSubmit={(event) => void submit(event)}>
|
||||||
|
<input value={name} onChange={(event) => setName(event.target.value)} placeholder="Имя" required />
|
||||||
|
<input value={phone} onChange={(event) => setPhone(event.target.value)} placeholder="Телефон" required />
|
||||||
|
<button type="submit">Подтвердить</button>
|
||||||
|
{error && <p className="error">{error}</p>}
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
web/app/globals.css
Normal file
16
web/app/globals.css
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
* { box-sizing: border-box; }
|
||||||
|
body { margin: 0; font-family: sans-serif; background: #f4f4f4; color: #171717; }
|
||||||
|
a { color: inherit; }
|
||||||
|
header { display: flex; gap: 16px; padding: 16px 20px; background: #fff; border-bottom: 1px solid #ddd; }
|
||||||
|
header a { font-weight: 600; text-decoration: none; }
|
||||||
|
main { max-width: 840px; margin: 0 auto; padding: 20px 16px 48px; }
|
||||||
|
.row { display: flex; flex-wrap: wrap; gap: 8px; margin: 12px 0 16px; }
|
||||||
|
.row a { background: #fff; border: 1px solid #ccc; padding: 6px 10px; text-decoration: none; }
|
||||||
|
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 12px; }
|
||||||
|
.card { background: #fff; border: 1px solid #ddd; padding: 12px; }
|
||||||
|
.card img, .product img { width: 100%; height: auto; display: block; background: #eee; }
|
||||||
|
.card h2, .product h1 { font-size: 1.1rem; margin: 10px 0 6px; }
|
||||||
|
.muted { color: #555; }
|
||||||
|
button, input { font: inherit; padding: 8px 10px; }
|
||||||
|
form { display: grid; gap: 8px; max-width: 360px; }
|
||||||
|
.error { color: #9f1239; }
|
||||||
3
web/app/health/route.ts
Normal file
3
web/app/health/route.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export function GET() {
|
||||||
|
return Response.json({ status: "ok" });
|
||||||
|
}
|
||||||
22
web/app/layout.tsx
Normal file
22
web/app/layout.tsx
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import type { ReactNode } from "react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Магазин",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<html lang="ru">
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<Link href="/">Каталог</Link>
|
||||||
|
<Link href="/cart">Корзина</Link>
|
||||||
|
</header>
|
||||||
|
<main>{children}</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
web/app/orders/[id]/page.tsx
Normal file
46
web/app/orders/[id]/page.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useParams } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import type { Order } from "@/lib/types";
|
||||||
|
|
||||||
|
export default function OrderPage() {
|
||||||
|
const params = useParams<{ id: string }>();
|
||||||
|
const [order, setOrder] = useState<Order | null>(null);
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void (async () => {
|
||||||
|
const response = await fetch(`/backend/orders/${params.id}`, { credentials: "include", cache: "no-store" });
|
||||||
|
if (!response.ok) {
|
||||||
|
setError("Заказ не найден");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setOrder(await response.json());
|
||||||
|
})();
|
||||||
|
}, [params.id]);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return <p className="error">{error}</p>;
|
||||||
|
}
|
||||||
|
if (!order) {
|
||||||
|
return <p>Загрузка…</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Заказ {order.id}</h1>
|
||||||
|
<p>Статус: создан, без оплаты.</p>
|
||||||
|
<p>
|
||||||
|
{order.customer_name}, {order.customer_phone}
|
||||||
|
</p>
|
||||||
|
{order.items.map((item) => (
|
||||||
|
<p key={item.product_id}>
|
||||||
|
{item.title} × {item.qty} — {item.price} ₽
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
<p>Итого: {order.total} ₽</p>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
58
web/app/page.tsx
Normal file
58
web/app/page.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
import { getCatalog } from "@/lib/api";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
const SORTS = [
|
||||||
|
["price_asc", "Сначала дешевле"],
|
||||||
|
["price_desc", "Сначала дороже"],
|
||||||
|
["new", "Новые"],
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export default async function CatalogPage({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: Promise<{ category_id?: string; sort?: string }>;
|
||||||
|
}) {
|
||||||
|
const params = await searchParams;
|
||||||
|
const sort = SORTS.some(([value]) => value === params.sort) ? params.sort! : "price_asc";
|
||||||
|
const catalog = await getCatalog(params.category_id, sort);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h1>Каталог</h1>
|
||||||
|
<div className="row">
|
||||||
|
<Link href={`/?sort=${sort}`}>Все</Link>
|
||||||
|
{catalog.categories.map((category) => (
|
||||||
|
<Link key={category.id} href={`/?category_id=${category.id}&sort=${sort}`}>
|
||||||
|
{category.name}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="row">
|
||||||
|
{SORTS.map(([value, label]) => (
|
||||||
|
<Link
|
||||||
|
key={value}
|
||||||
|
href={params.category_id ? `/?category_id=${params.category_id}&sort=${value}` : `/?sort=${value}`}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="grid">
|
||||||
|
{catalog.products.map((product) => (
|
||||||
|
<article className="card" key={product.id}>
|
||||||
|
<Link href={`/products/${product.id}`}>
|
||||||
|
<img src={`/backend/products/${product.id}/image`} alt="" />
|
||||||
|
<h2>{product.title}</h2>
|
||||||
|
</Link>
|
||||||
|
<p className="muted">{product.category_name}</p>
|
||||||
|
<p>{product.price} ₽</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{catalog.products.length === 0 && <p>В этой категории нет активных товаров.</p>}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
web/app/products/[id]/page.tsx
Normal file
20
web/app/products/[id]/page.tsx
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
import { AddToCart } from "@/components/AddToCart";
|
||||||
|
import { getProduct } from "@/lib/api";
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic";
|
||||||
|
|
||||||
|
export default async function ProductPage({ params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const { id } = await params;
|
||||||
|
const product = await getProduct(id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<article className="product">
|
||||||
|
<img src={`/backend/products/${product.id}/image`} alt="" />
|
||||||
|
<p className="muted">{product.category_name}</p>
|
||||||
|
<h1>{product.title}</h1>
|
||||||
|
<p>{product.description}</p>
|
||||||
|
<p>{product.price} ₽</p>
|
||||||
|
<AddToCart productId={product.id} />
|
||||||
|
</article>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
web/components/AddToCart.tsx
Normal file
27
web/components/AddToCart.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
export function AddToCart({ productId }: { productId: number }) {
|
||||||
|
const [message, setMessage] = useState("");
|
||||||
|
|
||||||
|
async function add() {
|
||||||
|
setMessage("Добавляю…");
|
||||||
|
const response = await fetch("/backend/cart/items", {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ product_id: productId, qty: 1 }),
|
||||||
|
});
|
||||||
|
setMessage(response.ok ? "В корзине" : "Не удалось добавить");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<p>
|
||||||
|
<button type="button" onClick={add}>
|
||||||
|
В корзину
|
||||||
|
</button>{" "}
|
||||||
|
{message}
|
||||||
|
</p>
|
||||||
|
);
|
||||||
|
}
|
||||||
34
web/lib/api.ts
Normal file
34
web/lib/api.ts
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import { notFound } from "next/navigation";
|
||||||
|
|
||||||
|
import type { Catalog, Product } from "./types";
|
||||||
|
|
||||||
|
function apiBase(): string {
|
||||||
|
const url = process.env.API_INTERNAL_URL;
|
||||||
|
if (!url) {
|
||||||
|
throw new Error("API_INTERNAL_URL is not set");
|
||||||
|
}
|
||||||
|
return url.replace(/\/$/, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getCatalog(categoryId?: string, sort = "price_asc"): Promise<Catalog> {
|
||||||
|
const params = new URLSearchParams({ sort });
|
||||||
|
if (categoryId) {
|
||||||
|
params.set("category_id", categoryId);
|
||||||
|
}
|
||||||
|
const response = await fetch(`${apiBase()}/products?${params}`, { cache: "no-store" });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Не удалось загрузить каталог");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getProduct(id: string): Promise<Product> {
|
||||||
|
const response = await fetch(`${apiBase()}/products/${id}`, { cache: "no-store" });
|
||||||
|
if (response.status === 404) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error("Не удалось загрузить товар");
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
48
web/lib/types.ts
Normal file
48
web/lib/types.ts
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
export type Category = {
|
||||||
|
id: number;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Product = {
|
||||||
|
id: number;
|
||||||
|
category_id: number;
|
||||||
|
category_name: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
price: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Catalog = {
|
||||||
|
categories: Category[];
|
||||||
|
products: Product[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CartItem = {
|
||||||
|
product_id: number;
|
||||||
|
title: string;
|
||||||
|
price: string;
|
||||||
|
qty: number;
|
||||||
|
line_total: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Cart = {
|
||||||
|
items: CartItem[];
|
||||||
|
total: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OrderItem = {
|
||||||
|
product_id: number;
|
||||||
|
title: string;
|
||||||
|
price: string;
|
||||||
|
qty: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Order = {
|
||||||
|
id: number;
|
||||||
|
status: string;
|
||||||
|
customer_name: string;
|
||||||
|
customer_phone: string;
|
||||||
|
total: string;
|
||||||
|
created_at: string;
|
||||||
|
items: OrderItem[];
|
||||||
|
};
|
||||||
2
web/next-env.d.ts
vendored
Normal file
2
web/next-env.d.ts
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
7
web/next.config.ts
Normal file
7
web/next.config.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
output: "standalone",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
20
web/package.json
Normal file
20
web/package.json
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "shop-web",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "^15.5.4",
|
||||||
|
"react": "^19.1.0",
|
||||||
|
"react-dom": "^19.1.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.15.0",
|
||||||
|
"@types/react": "^19.1.0",
|
||||||
|
"@types/react-dom": "^19.1.0",
|
||||||
|
"typescript": "^5.9.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
0
web/public/.gitkeep
Normal file
0
web/public/.gitkeep
Normal file
21
web/tsconfig.json
Normal file
21
web/tsconfig.json
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"paths": { "@/*": ["./*"] }
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user