summaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/app')
-rw-r--r--src/app/(desktop-screen)/result/page.tsx14
-rw-r--r--src/app/(mobile-screen)/layout.tsx12
-rw-r--r--src/app/(mobile-screen)/login/page.tsx8
-rw-r--r--src/app/(mobile-screen)/page.tsx16
-rw-r--r--src/app/api/auth/login/route.tsx36
-rw-r--r--src/app/api/company/route.tsx8
-rw-r--r--src/app/api/hash/route.tsx10
-rw-r--r--src/app/api/location/route.tsx27
-rw-r--r--src/app/api/product/import/route.tsx27
-rw-r--r--src/app/api/product/route.tsx48
-rw-r--r--src/app/api/stock-opname/location/route.tsx57
-rw-r--r--src/app/api/stock-opname/quantity/route.tsx34
-rw-r--r--src/app/api/stock-opname/route.tsx218
-rw-r--r--src/app/globals.css27
-rw-r--r--src/app/layout.tsx24
-rw-r--r--src/app/page.tsx113
16 files changed, 532 insertions, 147 deletions
diff --git a/src/app/(desktop-screen)/result/page.tsx b/src/app/(desktop-screen)/result/page.tsx
new file mode 100644
index 0000000..8c96272
--- /dev/null
+++ b/src/app/(desktop-screen)/result/page.tsx
@@ -0,0 +1,14 @@
+import Authenticated from "@/common/components/Authenticated"
+import Result from "@/modules/result"
+
+const ResultPage = () => {
+ return (
+ <Authenticated>
+ <main className="px-4 pt-6 bg-white">
+ <Result />
+ </main>
+ </Authenticated>
+ )
+}
+
+export default ResultPage \ No newline at end of file
diff --git a/src/app/(mobile-screen)/layout.tsx b/src/app/(mobile-screen)/layout.tsx
new file mode 100644
index 0000000..f44be2c
--- /dev/null
+++ b/src/app/(mobile-screen)/layout.tsx
@@ -0,0 +1,12 @@
+import ScreenContainer from '@/common/components/ScreenContainer'
+import React from 'react'
+
+const layout = ({ children }: { children: React.ReactNode }) => {
+ return (
+ <ScreenContainer>
+ {children}
+ </ScreenContainer>
+ )
+}
+
+export default layout \ No newline at end of file
diff --git a/src/app/(mobile-screen)/login/page.tsx b/src/app/(mobile-screen)/login/page.tsx
new file mode 100644
index 0000000..d518e2d
--- /dev/null
+++ b/src/app/(mobile-screen)/login/page.tsx
@@ -0,0 +1,8 @@
+import Login from '@/modules/login'
+import React from 'react'
+
+const LoginPage = () => {
+ return <Login />
+}
+
+export default LoginPage \ No newline at end of file
diff --git a/src/app/(mobile-screen)/page.tsx b/src/app/(mobile-screen)/page.tsx
new file mode 100644
index 0000000..430a195
--- /dev/null
+++ b/src/app/(mobile-screen)/page.tsx
@@ -0,0 +1,16 @@
+import Authenticated from "@/common/components/Authenticated";
+import ProfileCard from "@/modules/profile-card";
+import StockOpname from "@/modules/stock-opname";
+import { Spacer } from "@nextui-org/react";
+
+export default function HomePage() {
+ return (
+ <Authenticated>
+ <main className="px-4 pt-10">
+ <ProfileCard />
+ <Spacer y={6} />
+ <StockOpname />
+ </main>
+ </Authenticated>
+ )
+}
diff --git a/src/app/api/auth/login/route.tsx b/src/app/api/auth/login/route.tsx
new file mode 100644
index 0000000..d4da662
--- /dev/null
+++ b/src/app/api/auth/login/route.tsx
@@ -0,0 +1,36 @@
+import { NextRequest, NextResponse } from "next/server";
+import { prisma } from "prisma/client";
+import { cookies } from "next/headers"
+import { Credential } from "@/common/types/auth"
+import bcrypt from "bcrypt";
+import jwt from "jsonwebtoken";
+
+const JWT_SECRET = process.env.JWT_SECRET as string
+
+export async function POST(request: NextRequest) {
+ const body = await request.json()
+
+ const user = await prisma.user.findUnique({
+ where: { username: body.username },
+ include: {
+ company: true
+ }
+ })
+
+ if (!user) {
+ return NextResponse.json({ error: 'User not found' }, { status: 404 })
+ }
+
+ if (!await bcrypt.compare(body.password, user.password)) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const credential: Credential = {
+ ...user,
+ token: jwt.sign(user, JWT_SECRET, { expiresIn: '10y' })
+ }
+
+ cookies().set('credential', JSON.stringify(credential))
+
+ return NextResponse.json(credential)
+} \ No newline at end of file
diff --git a/src/app/api/company/route.tsx b/src/app/api/company/route.tsx
new file mode 100644
index 0000000..c970108
--- /dev/null
+++ b/src/app/api/company/route.tsx
@@ -0,0 +1,8 @@
+import { NextResponse } from "next/server";
+import { prisma } from "prisma/client";
+
+export async function GET() {
+ const companies = await prisma.company.findMany()
+
+ return NextResponse.json(companies)
+} \ No newline at end of file
diff --git a/src/app/api/hash/route.tsx b/src/app/api/hash/route.tsx
new file mode 100644
index 0000000..2727e1f
--- /dev/null
+++ b/src/app/api/hash/route.tsx
@@ -0,0 +1,10 @@
+import { NextRequest, NextResponse } from "next/server";
+import bcrypt from "bcrypt"
+
+export async function GET(request: NextRequest) {
+ const searchParams = request.nextUrl.searchParams;
+ const password = searchParams.get('password') ?? '';
+ const hash = await bcrypt.hash(password, 10);
+
+ return NextResponse.json({ hash });
+} \ No newline at end of file
diff --git a/src/app/api/location/route.tsx b/src/app/api/location/route.tsx
new file mode 100644
index 0000000..452a85d
--- /dev/null
+++ b/src/app/api/location/route.tsx
@@ -0,0 +1,27 @@
+import { NextRequest, NextResponse } from "next/server";
+import { prisma } from "prisma/client";
+import { Credential } from "@/common/types/auth"
+
+export async function GET(request: NextRequest) {
+ const searchParams = request.nextUrl.searchParams;
+ const search = searchParams.get('search');
+
+ const credentialStr = request.cookies.get('credential')?.value
+ const credential: Credential | null = credentialStr ? JSON.parse(credentialStr) : null
+
+ if (!credential) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const { companyId } = credential
+
+ const locations = await prisma.location.findMany({
+ where: {
+ companyId,
+ name: { contains: search ?? '' }
+ },
+ take: 20
+ })
+
+ return NextResponse.json(locations)
+} \ No newline at end of file
diff --git a/src/app/api/product/import/route.tsx b/src/app/api/product/import/route.tsx
new file mode 100644
index 0000000..7358205
--- /dev/null
+++ b/src/app/api/product/import/route.tsx
@@ -0,0 +1,27 @@
+import { NextRequest, NextResponse } from "next/server";
+import { prisma } from "prisma/client";
+import * as XLSX from "xlsx";
+
+export async function POST(request: NextRequest) {
+ const body = await request.arrayBuffer();
+ const workbook = XLSX.read(body, { type: 'buffer' })
+ const worksheetName = workbook.SheetNames[0]
+ const worksheet = workbook.Sheets[worksheetName]
+ const fileData: any[] = XLSX.utils.sheet_to_json(worksheet, { header: 1 })
+ fileData.shift();
+
+ const newProducts = fileData.map(row => ({
+ id: undefined,
+ isDifferent: false,
+ name: row[0],
+ barcode: row[1],
+ itemCode: row[2],
+ onhandQty: row[3],
+ differenceQty: row[4],
+ companyId: row[5],
+ }));
+
+ await prisma.product.createMany({ data: newProducts })
+
+ return NextResponse.json(true)
+} \ No newline at end of file
diff --git a/src/app/api/product/route.tsx b/src/app/api/product/route.tsx
new file mode 100644
index 0000000..1161a4e
--- /dev/null
+++ b/src/app/api/product/route.tsx
@@ -0,0 +1,48 @@
+import { NextRequest, NextResponse } from "next/server";
+import { prisma } from "prisma/client";
+import { Credential } from "@/common/types/auth"
+
+export async function GET(request: NextRequest) {
+ const PAGE_SIZE = 30;
+
+ const searchParams = request.nextUrl.searchParams;
+ const type = searchParams.get('type')
+ const search = searchParams.get('search');
+ const page = searchParams.get('page');
+ const intPage: number = page ? parseInt(page) : 1;
+
+ const credentialStr = request.cookies.get('credential')?.value
+ const credential: Credential | null = credentialStr ? JSON.parse(credentialStr) : null
+
+ if (!credential) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+ }
+
+ const { companyId } = credential
+
+ const where = {
+ AND: {
+ OR: [
+ { name: { contains: search ?? '' } },
+ { barcode: { contains: search ?? '' } },
+ { itemCode: { contains: search ?? '' } },
+ ],
+ companyId: type == 'all' ? undefined : companyId
+ }
+ }
+
+ const products = await prisma.product.findMany({
+ where,
+ include: { company: true },
+ take: PAGE_SIZE,
+ skip: (intPage - 1) * PAGE_SIZE
+ })
+
+ const count = await prisma.product.count({ where })
+ const pagination = {
+ page: intPage,
+ totalPage: Math.ceil(count / PAGE_SIZE),
+ }
+
+ return NextResponse.json({ products, ...pagination })
+} \ No newline at end of file
diff --git a/src/app/api/stock-opname/location/route.tsx b/src/app/api/stock-opname/location/route.tsx
new file mode 100644
index 0000000..1009486
--- /dev/null
+++ b/src/app/api/stock-opname/location/route.tsx
@@ -0,0 +1,57 @@
+import { DetailTeam, StockOpnameLocationRes } from "@/common/types/stockOpname";
+import { Location, Team } from "@prisma/client";
+import { NextRequest, NextResponse } from "next/server";
+import { prisma } from "prisma/client";
+
+
+
+const getOpnameQuantity = async (where: { locationId: number, productId: number }) => {
+ const detailTeam: DetailTeam = {
+ COUNT1: { quantity: undefined, user: undefined },
+ COUNT2: { quantity: undefined, user: undefined },
+ VERIFICATION: { quantity: undefined, user: undefined },
+ }
+ for (const team of Object.keys(Team)) {
+ const opname = await prisma.stockOpname.findFirst({
+ where: { ...where, team: team as Team },
+ select: { quantity: true, user: true },
+ })
+ if (!opname) continue
+ detailTeam[team as Team]['quantity'] = opname?.quantity
+ detailTeam[team as Team]['user'] = opname?.user
+ }
+ return detailTeam
+}
+
+export async function GET(request: NextRequest) {
+ const searchParams = request.nextUrl.searchParams;
+ const productId = searchParams.get('productId') ?? '';
+ const companyId = searchParams.get('companyId');
+ const intProductId = parseInt(productId);
+
+ if (!companyId) {
+ return NextResponse.json({ error: 'Bad Request. Missing companyId' }, { status: 400 })
+ }
+
+ const intCompanyId = parseInt(companyId);
+
+ const opnameLocByProduct = await prisma.stockOpname.groupBy({
+ by: ['locationId'],
+ where: { productId: intProductId, companyId: intCompanyId },
+ })
+
+ const locationIds = opnameLocByProduct.map((opname) => opname.locationId)
+
+ const result: StockOpnameLocationRes[] = []
+
+ for (const locationId of locationIds) {
+ const detail = await getOpnameQuantity({ locationId, productId: intProductId })
+ const location = await prisma.location.findFirst({
+ where: { id: locationId, companyId: intCompanyId }
+ })
+ if (!location) continue
+ result.push({ ...location, ...detail })
+ }
+
+ return NextResponse.json(result)
+} \ No newline at end of file
diff --git a/src/app/api/stock-opname/quantity/route.tsx b/src/app/api/stock-opname/quantity/route.tsx
new file mode 100644
index 0000000..621297f
--- /dev/null
+++ b/src/app/api/stock-opname/quantity/route.tsx
@@ -0,0 +1,34 @@
+import { Credential } from "@/common/types/auth";
+import { NextRequest, NextResponse } from "next/server";
+import { prisma } from "prisma/client";
+
+export async function GET(request: NextRequest) {
+ const credentialStr = request.cookies.get('credential')?.value
+ const credential: Credential | null = credentialStr ? JSON.parse(credentialStr) : null
+
+ if (!credential) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+
+ const searchParams = request.nextUrl.searchParams;
+ let locationId = searchParams.get('locationId');
+ let productId = searchParams.get('productId');
+
+ if (!locationId || !productId) return NextResponse.json({ error: 'Bad Request. Missing locationId and productId' }, { status: 400 })
+ let intLocationId = parseInt(locationId)
+ let intProductId = parseInt(productId)
+
+ const { companyId, team } = credential
+
+ const query = {
+ locationId: intLocationId,
+ productId: intProductId,
+ companyId,
+ team
+ }
+
+ const stockOpname = await prisma.stockOpname.findFirst({
+ where: query,
+ select: { id: true, quantity: true, user: true }
+ })
+
+ return NextResponse.json(stockOpname)
+} \ No newline at end of file
diff --git a/src/app/api/stock-opname/route.tsx b/src/app/api/stock-opname/route.tsx
new file mode 100644
index 0000000..50feacd
--- /dev/null
+++ b/src/app/api/stock-opname/route.tsx
@@ -0,0 +1,218 @@
+import { Credential } from "@/common/types/auth";
+import { StockOpnameLocationRes, StockOpnameRequest } from "@/common/types/stockOpname";
+import { Team } from "@prisma/client";
+import { NextRequest, NextResponse } from "next/server";
+import { prisma } from "prisma/client";
+
+type Quantity = {
+ [key in keyof typeof Team]: number | null
+}
+
+const calculateOpnameQuantity = async (
+ where: {
+ productId: number,
+ companyId: number
+ }
+): Promise<Quantity> => {
+ const quantity: Quantity = {
+ COUNT1: null,
+ COUNT2: null,
+ VERIFICATION: null,
+ }
+ for (const team of Object.values(Team)) {
+ const opnameQty = await prisma.stockOpname.groupBy({
+ by: ['productId', 'team'],
+ _sum: { quantity: true },
+ where: { team, ...where }
+ })
+ if (opnameQty.length === 0) continue
+ quantity[team] = opnameQty[0]._sum.quantity
+ }
+ return quantity
+}
+
+export async function GET(request: NextRequest) {
+ const PAGE_SIZE = 30;
+
+ const params = request.nextUrl.searchParams
+ const companyId = params.get('companyId')
+ const search = params.get('search')
+ const page = params.get('page') ?? null;
+ const intPage = page ? parseInt(page) : 1;
+
+ if (!companyId) {
+ return NextResponse.json({ error: 'Bad Request. Missing companyId' }, { status: 400 })
+ }
+
+ const where = {
+ AND: {
+ stockOpnames: { some: {} },
+ companyId: parseInt(companyId),
+ OR: [
+ { name: { contains: search ?? '' } },
+ { itemCode: { contains: search ?? '' } },
+ { barcode: { contains: search ?? '' } },
+ ]
+ }
+ }
+
+ const products = await prisma.product.findMany({
+ skip: (intPage - 1) * PAGE_SIZE,
+ take: PAGE_SIZE,
+ where,
+ select: {
+ id: true,
+ name: true,
+ itemCode: true,
+ barcode: true,
+ onhandQty: true,
+ differenceQty: true,
+ isDifferent: true
+ }
+ })
+
+ const productCount = await prisma.product.count({ where })
+
+ const pagination = {
+ page: intPage,
+ totalPage: Math.ceil(productCount / PAGE_SIZE),
+ }
+
+ type ProductWithSum = typeof products[0] & { quantity: Quantity }
+
+ const productsWithSum: ProductWithSum[] = []
+
+ for (const product of products) {
+ const quantity = await calculateOpnameQuantity({
+ productId: product.id,
+ companyId: parseInt(companyId)
+ })
+ productsWithSum.push({ ...product, quantity })
+ }
+
+ return NextResponse.json({
+ result: productsWithSum,
+ ...pagination
+ })
+}
+
+export async function POST(request: NextRequest) {
+ const credentialStr = request.cookies.get('credential')?.value
+ const credential: Credential | null = credentialStr ? JSON.parse(credentialStr) : null
+
+ if (!credential) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
+
+ const body: StockOpnameRequest = await request.json()
+
+ const { companyId, team } = credential
+
+ const query = {
+ locationId: body.location,
+ productId: body.product,
+ companyId,
+ team
+ }
+
+ const stockOpname = await prisma.stockOpname.findFirst({
+ where: query
+ })
+
+ const data = {
+ ...query,
+ userId: credential.id,
+ quantity: body.quantity,
+ isDifferent: false
+ }
+
+ let newStockOpname = null
+
+ if (!stockOpname) {
+ newStockOpname = await prisma.stockOpname.create({ data })
+ } else {
+ newStockOpname = await prisma.stockOpname.update({
+ where: { id: stockOpname.id },
+ data
+ })
+ }
+
+ computeIsDifferent({ productId: body.product, companyId: companyId })
+
+ return NextResponse.json(newStockOpname)
+}
+
+const SELF_HOST = process.env.SELF_HOST as string
+
+const computeIsDifferent = async ({
+ companyId,
+ productId
+}: {
+ companyId: number,
+ productId: number
+}) => {
+ const totalQty: { [key in keyof typeof Team]: number | null } = {
+ COUNT1: null,
+ COUNT2: null,
+ VERIFICATION: null
+ }
+
+ const searchParams = new URLSearchParams({
+ companyId: companyId.toString(),
+ productId: productId.toString()
+ })
+
+ const stockOpnamesFetch = await fetch(`${SELF_HOST}/api/stock-opname/location?${searchParams}`)
+ const stockOpnames: StockOpnameLocationRes[] = await stockOpnamesFetch.json()
+
+ const count2Count = await prisma.stockOpname.count({
+ where: { companyId, productId, team: 'COUNT2' }
+ })
+
+ const isCount2Counted: boolean = count2Count > 0
+
+ let isDifferent: boolean = false
+
+ for (const opname of stockOpnames) {
+ let { COUNT1, COUNT2, VERIFICATION } = opname
+
+ if (!totalQty['COUNT1'] && COUNT1.quantity) totalQty['COUNT1'] = 0
+ if (!totalQty['COUNT2'] && COUNT2.quantity) totalQty['COUNT2'] = 0
+ if (!totalQty['VERIFICATION'] && VERIFICATION.quantity) totalQty['VERIFICATION'] = 0
+
+ if (totalQty['COUNT1'] !== null && COUNT1.quantity) totalQty['COUNT1'] += COUNT1.quantity
+ if (totalQty['COUNT2'] !== null && COUNT2.quantity) totalQty['COUNT2'] += COUNT2.quantity
+ if (totalQty['VERIFICATION'] !== null && VERIFICATION.quantity) totalQty['VERIFICATION'] += VERIFICATION.quantity
+
+ if (isCount2Counted && COUNT1.quantity != COUNT2.quantity) {
+ isDifferent = true
+ }
+ }
+
+ const product = await prisma.product.findFirst({
+ where: { id: productId }
+ })
+ if (!product) return
+
+ const onhandQty = product?.onhandQty || 0
+
+ if (!isDifferent) {
+ if (
+ (typeof totalQty['VERIFICATION'] === 'number' && totalQty['VERIFICATION'] > 0) ||
+ totalQty['COUNT2'] == onhandQty ||
+ totalQty['COUNT1'] == onhandQty ||
+ totalQty['COUNT1'] == totalQty['COUNT2']
+ ) {
+ isDifferent = false
+ } else {
+ isDifferent = true
+ }
+
+ if (isCount2Counted && totalQty['COUNT1'] != onhandQty) {
+ isDifferent = true
+ }
+ }
+
+ await prisma.product.update({
+ where: { id: product.id },
+ data: { isDifferent }
+ })
+} \ No newline at end of file
diff --git a/src/app/globals.css b/src/app/globals.css
deleted file mode 100644
index fd81e88..0000000
--- a/src/app/globals.css
+++ /dev/null
@@ -1,27 +0,0 @@
-@tailwind base;
-@tailwind components;
-@tailwind utilities;
-
-:root {
- --foreground-rgb: 0, 0, 0;
- --background-start-rgb: 214, 219, 220;
- --background-end-rgb: 255, 255, 255;
-}
-
-@media (prefers-color-scheme: dark) {
- :root {
- --foreground-rgb: 255, 255, 255;
- --background-start-rgb: 0, 0, 0;
- --background-end-rgb: 0, 0, 0;
- }
-}
-
-body {
- color: rgb(var(--foreground-rgb));
- background: linear-gradient(
- to bottom,
- transparent,
- rgb(var(--background-end-rgb))
- )
- rgb(var(--background-start-rgb));
-}
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index 40e027f..933e308 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,12 +1,15 @@
import type { Metadata } from 'next'
-import { Inter } from 'next/font/google'
-import './globals.css'
-
-const inter = Inter({ subsets: ['latin'] })
+import '@/common/styles/globals.css'
+import clsxm from '@/common/libs/clsxm'
+import { inter } from '@/common/styles/fonts'
+import UIProvider from '@/common/contexts/UIProvider'
+import QueryProvider from '@/common/contexts/QueryProvider'
+import { Toaster } from 'react-hot-toast'
export const metadata: Metadata = {
- title: 'Create Next App',
- description: 'Generated by create next app',
+ title: 'FIN Stock Opname',
+ description: 'FIN Stock Opname',
+ viewport: 'width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0',
}
export default function RootLayout({
@@ -16,7 +19,14 @@ export default function RootLayout({
}) {
return (
<html lang="en">
- <body className={inter.className}>{children}</body>
+ <body className={clsxm(inter.variable)}>
+ <Toaster />
+ <UIProvider>
+ <QueryProvider>
+ {children}
+ </QueryProvider>
+ </UIProvider>
+ </body>
</html>
)
}
diff --git a/src/app/page.tsx b/src/app/page.tsx
deleted file mode 100644
index e38c626..0000000
--- a/src/app/page.tsx
+++ /dev/null
@@ -1,113 +0,0 @@
-import Image from 'next/image'
-
-export default function Home() {
- return (
- <main className="flex min-h-screen flex-col items-center justify-between p-24">
- <div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
- <p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
- Get started by editing&nbsp;
- <code className="font-mono font-bold">src/app/page.tsx</code>
- </p>
- <div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
- <a
- className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
- href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
- target="_blank"
- rel="noopener noreferrer"
- >
- By{' '}
- <Image
- src="/vercel.svg"
- alt="Vercel Logo"
- className="dark:invert"
- width={100}
- height={24}
- priority
- />
- </a>
- </div>
- </div>
-
- <div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
- <Image
- className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
- src="/next.svg"
- alt="Next.js Logo"
- width={180}
- height={37}
- priority
- />
- </div>
-
- <div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
- <a
- href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
- className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
- target="_blank"
- rel="noopener noreferrer"
- >
- <h2 className={`mb-3 text-2xl font-semibold`}>
- Docs{' '}
- <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
- -&gt;
- </span>
- </h2>
- <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
- Find in-depth information about Next.js features and API.
- </p>
- </a>
-
- <a
- href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
- className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
- target="_blank"
- rel="noopener noreferrer"
- >
- <h2 className={`mb-3 text-2xl font-semibold`}>
- Learn{' '}
- <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
- -&gt;
- </span>
- </h2>
- <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
- Learn about Next.js in an interactive course with&nbsp;quizzes!
- </p>
- </a>
-
- <a
- href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
- className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
- target="_blank"
- rel="noopener noreferrer"
- >
- <h2 className={`mb-3 text-2xl font-semibold`}>
- Templates{' '}
- <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
- -&gt;
- </span>
- </h2>
- <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
- Explore the Next.js 13 playground.
- </p>
- </a>
-
- <a
- href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
- className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
- target="_blank"
- rel="noopener noreferrer"
- >
- <h2 className={`mb-3 text-2xl font-semibold`}>
- Deploy{' '}
- <span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
- -&gt;
- </span>
- </h2>
- <p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
- Instantly deploy your Next.js site to a shareable URL with Vercel.
- </p>
- </a>
- </div>
- </main>
- )
-}