lushdog@outlook.com před 2 týdny
rodič
revize
f89e4a0225

+ 53 - 0
.dockerignore

@@ -0,0 +1,53 @@
+# 依赖
+node_modules
+.pnp
+.pnp.js
+
+# 测试
+coverage
+*.test.ts
+*.test.tsx
+*.spec.ts
+*.spec.tsx
+
+# Next.js
+.next
+out
+build
+dist
+
+# 环境变量
+.env
+.env*.local
+
+# 日志
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+# 编辑器
+.vscode
+.idea
+*.swp
+*.swo
+*~
+
+# 操作系统
+.DS_Store
+Thumbs.db
+
+# Git
+.git
+.gitignore
+
+# Docker
+Dockerfile
+.dockerignore
+
+# 其他
+README.md
+*.md
+.eslintcache
+

+ 12 - 0
.prettierignore

@@ -0,0 +1,12 @@
+node_modules
+.next
+out
+build
+dist
+*.lock
+pnpm-lock.yaml
+package-lock.json
+yarn.lock
+.DS_Store
+*.log
+

+ 9 - 0
.prettierrc.json

@@ -0,0 +1,9 @@
+{
+  "semi": false,
+  "singleQuote": true,
+  "trailingComma": "es5",
+  "tabWidth": 2,
+  "useTabs": false,
+  "printWidth": 80,
+  "arrowParens": "always"
+}

+ 56 - 0
Dockerfile

@@ -0,0 +1,56 @@
+# 使用 Node.js 官方镜像作为基础镜像
+FROM node:20-alpine AS base
+
+# 安装 pnpm
+RUN corepack enable && corepack prepare pnpm@latest --activate
+
+# 设置工作目录
+WORKDIR /app
+
+# 依赖安装阶段
+FROM base AS deps
+# 复制包管理文件
+COPY package.json pnpm-lock.yaml ./
+# 安装依赖
+RUN pnpm install --frozen-lockfile
+
+# 构建阶段
+FROM base AS builder
+# 复制依赖
+COPY --from=deps /app/node_modules ./node_modules
+# 复制源代码
+COPY . .
+# 构建应用
+RUN pnpm build
+
+# 运行阶段
+FROM base AS runner
+WORKDIR /app
+
+ENV NODE_ENV=production
+# 禁用 Next.js 遥测
+ENV NEXT_TELEMETRY_DISABLED=1
+
+# 创建非 root 用户
+RUN addgroup --system --gid 1001 nodejs
+RUN adduser --system --uid 1001 nextjs
+
+# 复制必要的文件
+COPY --from=builder /app/public ./public
+COPY --from=builder /app/.next/standalone ./
+COPY --from=builder /app/.next/static ./.next/static
+
+# 设置正确的权限
+RUN chown -R nextjs:nodejs /app
+
+USER nextjs
+
+# 暴露端口
+EXPOSE 3000
+
+ENV PORT=3000
+ENV HOSTNAME="0.0.0.0"
+
+# 启动应用
+CMD ["node", "server.js"]
+

+ 5 - 5
eslint.config.mjs

@@ -1,6 +1,6 @@
-import { defineConfig, globalIgnores } from "eslint/config";
-import nextVitals from "eslint-config-next/core-web-vitals";
-import nextTs from "eslint-config-next/typescript";
+import { defineConfig, globalIgnores } from "eslint/config"
+import nextVitals from "eslint-config-next/core-web-vitals"
+import nextTs from "eslint-config-next/typescript"
 
 const eslintConfig = defineConfig([
   ...nextVitals,
@@ -13,6 +13,6 @@ const eslintConfig = defineConfig([
     "build/**",
     "next-env.d.ts",
   ]),
-]);
+])
 
-export default eslintConfig;
+export default eslintConfig

+ 4 - 4
next.config.ts

@@ -1,7 +1,7 @@
-import type { NextConfig } from "next";
+import type { NextConfig } from 'next'
 
 const nextConfig: NextConfig = {
-  /* config options here */
-};
+  output: 'standalone',
+}
 
-export default nextConfig;
+export default nextConfig

+ 5 - 1
package.json

@@ -6,9 +6,12 @@
     "dev": "next dev",
     "build": "next build",
     "start": "next start",
-    "lint": "eslint"
+    "lint": "eslint",
+    "format": "prettier --write .",
+    "format:check": "prettier --check ."
   },
   "dependencies": {
+    "antd": "^6.0.1",
     "next": "16.0.7",
     "react": "19.2.0",
     "react-dom": "19.2.0"
@@ -20,6 +23,7 @@
     "@types/react-dom": "^19",
     "eslint": "^9",
     "eslint-config-next": "16.0.7",
+    "prettier": "^3.7.4",
     "tailwindcss": "^4",
     "typescript": "^5"
   }

Rozdílová data souboru nebyla zobrazena, protože soubor je příliš velký
+ 466 - 127
pnpm-lock.yaml


+ 2 - 2
postcss.config.mjs

@@ -2,6 +2,6 @@ const config = {
   plugins: {
     "@tailwindcss/postcss": {},
   },
-};
+}
 
-export default config;
+export default config

+ 36 - 0
src/app/api/pools/list/route.ts

@@ -0,0 +1,36 @@
+import { NextRequest, NextResponse } from 'next/server'
+
+export async function GET(request: NextRequest) {
+  try {
+    const searchParams = request.nextUrl.searchParams
+    const page = searchParams.get('page') || '1'
+    const pageSize = searchParams.get('pageSize') || '500'
+
+    const response = await fetch(
+      `https://api2.byreal.io/byreal/api/dex/v2/pools/info/list?page=${page}&pageSize=${pageSize}`,
+      {
+        method: 'GET',
+        headers: {
+          accept: 'application/json',
+        },
+      }
+    )
+
+    if (!response.ok) {
+      return NextResponse.json(
+        { retCode: response.status, retMsg: '外部 API 请求失败' },
+        { status: response.status }
+      )
+    }
+
+    const data = await response.json()
+    return NextResponse.json(data)
+  } catch (error) {
+    console.error('API Route Error:', error)
+    return NextResponse.json(
+      { retCode: 500, retMsg: '服务器内部错误' },
+      { status: 500 }
+    )
+  }
+}
+

+ 58 - 0
src/app/api/top-positions/route.ts

@@ -0,0 +1,58 @@
+import { NextRequest, NextResponse } from 'next/server'
+
+interface RequestBody {
+  poolAddress?: string
+  parentPositionAddress?: string
+  page?: number
+  pageSize?: number
+  sortField?: string
+  status?: number
+}
+
+export async function POST(request: NextRequest) {
+  try {
+    const body: RequestBody = await request.json()
+
+    // console.log(body.pageSize)
+
+    const response = await fetch(
+      'https://api2.byreal.io/byreal/api/dex/v2/copyfarmer/top-positions',
+      {
+        method: 'POST',
+        headers: {
+          accept: 'application/json',
+          'content-type': 'application/json',
+        },
+        body: JSON.stringify({
+          poolAddress:
+            body.poolAddress ||
+            'FPBW9dtVRoUug2BeUKZAzaknd6iiet9jHM8RcTvwUkyC',
+          ...(body.parentPositionAddress && {
+            parentPositionAddress: body.parentPositionAddress,
+          }),
+          page: body.page || 1,
+          pageSize: body.pageSize || 5,
+          sortField: body.sortField || 'liquidity',
+          status: body.status ?? 0,
+        }),
+      }
+    )
+
+    if (!response.ok) {
+      return NextResponse.json(
+        { code: response.status, message: '外部 API 请求失败' },
+        { status: response.status }
+      )
+    }
+
+    const data = await response.json()
+    return NextResponse.json(data)
+  } catch (error) {
+    console.error('API Route Error:', error)
+    return NextResponse.json(
+      { code: 500, message: '服务器内部错误' },
+      { status: 500 }
+    )
+  }
+}
+

+ 561 - 0
src/app/components/DataTable.tsx

@@ -0,0 +1,561 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { Table, message, Select, Typography, Tag, Button } from 'antd'
+// import type { ColumnsType } from 'antd/es/table'
+
+interface TableData {
+  key: string
+  [key: string]: unknown
+}
+
+interface ApiResponse {
+  retCode: number
+  result: {
+    data: {
+      records: Record<string, unknown>[]
+      total: number
+      current: number
+      pageSize: number
+      pages: number
+    }
+  }
+  retMsg?: string
+}
+
+interface PoolOption {
+  label: string
+  value: string
+}
+
+interface PoolInfo {
+  poolAddress: string
+  mintA?: {
+    mintInfo?: {
+      symbol?: string
+    }
+  }
+  mintB?: {
+    mintInfo?: {
+      symbol?: string
+    }
+  }
+}
+
+interface PoolsListResponse {
+  retCode: number
+  result?: {
+    data?: {
+      records?: PoolInfo[]
+    }
+  }
+  retMsg?: string
+}
+
+export default function DataTable() {
+  const [data, setData] = useState<TableData[]>([])
+  const [loading, setLoading] = useState(true)
+  const [poolOptions, setPoolOptions] = useState<PoolOption[]>([])
+  const [selectedPoolAddress, setSelectedPoolAddress] = useState<string>(
+    'FPBW9dtVRoUug2BeUKZAzaknd6iiet9jHM8RcTvwUkyC'
+  )
+  const [pagination, setPagination] = useState({
+    current: 1,
+    pageSize: 100,
+    total: 0,
+  })
+  const [sortField, setSortField] = useState<string>('liquidity')
+  const [expandedRowKeys, setExpandedRowKeys] = useState<React.Key[]>([])
+  const [childTableData, setChildTableData] = useState<
+    Record<string, TableData[]>
+  >({})
+  const [childTableLoading, setChildTableLoading] = useState<
+    Record<string, boolean>
+  >({})
+
+  // 获取 pools 列表
+  const fetchPoolsList = async () => {
+    try {
+      const response = await fetch('/api/pools/list?page=1&pageSize=500')
+      if (!response.ok) {
+        throw new Error(`HTTP error! status: ${response.status}`)
+      }
+
+      const result: PoolsListResponse = await response.json()
+      if (result.result?.data?.records) {
+        const options: PoolOption[] = result.result.data.records.map(
+          (pool) => {
+            const symbolA = pool.mintA?.mintInfo?.symbol || ''
+            const symbolB = pool.mintB?.mintInfo?.symbol || ''
+            const label = `${symbolA}/${symbolB}`
+            return {
+              label,
+              value: pool.poolAddress,
+            }
+          }
+        )
+        console.log(options)
+        setPoolOptions(options)
+      }
+    } catch (error) {
+      console.error('Error fetching pools list:', error)
+      message.error('获取 pools 列表失败')
+    }
+  }
+
+  const fetchData = async (
+    page: number = 1,
+    pageSize: number = 100,
+    poolAddress?: string,
+    currentSortField?: string
+  ) => {
+    setLoading(true)
+    try {
+      const response = await fetch('/api/top-positions', {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+        },
+        body: JSON.stringify({
+          poolAddress: poolAddress || selectedPoolAddress,
+          page,
+          pageSize,
+          sortField: currentSortField || sortField,
+          status: 0,
+        }),
+      })
+      // console.log(response.json())
+      if (!response.ok) {
+        throw new Error(`HTTP error! status: ${response.status}`)
+      }
+
+      const result: ApiResponse = await response.json()
+      console.log(result)
+      if (result.result) {
+        const { records, total, current, pageSize } = result.result.data
+        setData(records.map((item, index) => ({
+          key: `${item.id || index}`,
+          ...item,
+        })))
+        setPagination({
+          current: current,
+          pageSize: pageSize,
+          total: total,
+        })
+      } else {
+        message.error(result.retMsg || '获取数据失败')
+      }
+    } catch (error) {
+      console.error('Error fetching data:', error)
+      message.error('网络请求失败,请稍后重试')
+    } finally {
+      setLoading(false)
+    }
+  }
+
+  const init = async () => {
+    await fetchPoolsList()
+    await fetchData(1, 100)
+  }
+
+  useEffect(() => {
+    init()
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [])
+
+  const columns = [
+    {
+      title: '创建地址',
+      dataIndex: 'walletAddress',
+      key: 'walletAddress',
+      render: (text: string) => {
+        return <span className="font-bold text-lg">{text.slice(0, 6)}...{text.slice(-4)}</span>
+      }
+    },
+    {
+      title: 'Liquidity',
+      dataIndex: 'liquidityUsd',
+      key: 'liquidityUsd',
+      sorter: true,
+      render: (text: string) => {
+        return <span className="text-orange-500 font-bold text-lg" style={{ color: '#00B098'}}>${Number(text).toFixed(2)}</span>
+      }
+    },
+    {
+      title: '复制数',
+      dataIndex: 'copies',
+      key: 'copies',
+      sorter: true,
+      render: (text: string) => {
+        return <span className="text-green-500 font-bold text-lg">{text}</span>
+      }
+    },
+    {
+      title: '奖励',
+      dataIndex: 'bonusUsd',
+      key: 'bonusUsd',
+      render: (text: string) => {
+        return <span className="text-orange-500 font-bold text-lg">${Number(text).toFixed(2)}</span>
+      }
+    },
+    // {
+    //   title: 'APR',
+    //   dataIndex: 'bonusUsd',
+    //   key: 'bonusUsd',
+    //   render: (text: string) => {
+    //     return <span className="text-orange-500 font-bold text-lg">{Number(text).toFixed(2)}</span>
+    //   }
+    // },
+    {
+      title: 'FEE',
+      dataIndex: 'earnedUsd',
+      key: 'earnedUsd',
+      render: (text: string) => {
+        return <span className="text-orange-500 font-bold text-lg" style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000'}}>${Number(text).toFixed(2)}</span>
+      }
+    },
+    {
+      title: 'PNL',
+      dataIndex: 'pnlUsd',
+      key: 'pnlUsd',
+      render: (text: string) => {
+        return <span className="text-orange-500 font-bold text-lg" style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000'}}>${Number(text).toFixed(2)}</span>
+      }
+    },
+    // {
+    //   title: '创建时间',
+    //   dataIndex: 'positionAgeMs',
+    //   key: 'positionAgeMs',
+    //   render: (text: string) => {
+    //     // positionAgeMs 转化成时间,格式为小时
+    //     return <span className="font-bold text-lg">{Math.floor(Number(text) / 1000 / 60 / 60)} 小时</span>
+    //   }
+    // },
+    {
+      title: '创建时间',
+      dataIndex: 'positionAgeMs',
+      key: 'positionAgeMs',
+      render: (text: string) => {
+        return <span className="font-bold text-lg">{Math.floor(Number(text) / 1000 / 60 / 60)} 小时</span>
+      }
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (status: number) => {
+        return status === 0 ? <Tag color="green">Active</Tag> : <Tag color="red">Inactive</Tag>
+      }
+    },
+    {
+      title: '操作',
+      dataIndex: 'walletAddress',
+      key: 'walletAddress',
+      render: (text: string, record: TableData) => {
+        return <Typography.Link href={`https://www.byreal.io/en/portfolio?userAddress=${text}&tab=current&positionAddress=${record.positionAddress}`} target="_blank">复制</Typography.Link>
+      }
+    }
+    
+  ]
+
+  const handleTableChange = (
+    newPagination: {
+      current?: number
+      pageSize?: number
+    },
+    _filters: unknown,
+    sorter: {
+      field?: string
+      order?: 'ascend' | 'descend' | null
+    } | unknown
+  ) => {
+    let currentSortField = sortField
+    let shouldResetPage = false
+
+    if (sorter && typeof sorter === 'object' && 'field' in sorter) {
+      const sorterObj = sorter as {
+        field?: string
+        order?: 'ascend' | 'descend' | null
+      }
+      if (sorterObj.field && sorterObj.order) {
+        // 映射字段名到 API 的 sortField
+        const fieldMap: Record<string, string> = {
+          copies: 'copies',
+          earnedUsd: 'earnedUsd',
+          pnlUsd: 'pnlUsd',
+          liquidityUsd: 'liquidity',
+          positionAgeMs: 'positionAgeMs',
+        }
+        const newSortField = fieldMap[sorterObj.field] || sortField
+        if (newSortField !== sortField) {
+          currentSortField = newSortField
+          shouldResetPage = true
+        }
+      }
+    }
+
+    setSortField(currentSortField)
+    const targetPage = shouldResetPage ? 1 : newPagination.current || pagination.current
+    const targetPageSize = newPagination.pageSize || pagination.pageSize
+
+    setPagination({
+      ...pagination,
+      current: targetPage,
+      pageSize: targetPageSize,
+    })
+
+    fetchData(
+      targetPage,
+      targetPageSize,
+      selectedPoolAddress,
+      currentSortField
+    )
+  }
+
+  const handlePoolChange = (value: string) => {
+    setSelectedPoolAddress(value)
+    fetchData(1, 100, value)
+  }
+
+  // 获取子表格数据
+  const fetchChildData = async (parentPositionAddress: string) => {
+    if (childTableData[parentPositionAddress]) {
+      // 如果已经加载过,直接返回
+      return
+    }
+
+    setChildTableLoading((prev) => ({
+      ...prev,
+      [parentPositionAddress]: true,
+    }))
+
+    try {
+      const response = await fetch('/api/top-positions', {
+        method: 'POST',
+        headers: {
+          'content-type': 'application/json',
+        },
+        body: JSON.stringify({
+          poolAddress: selectedPoolAddress,
+          parentPositionAddress,
+          page: 1,
+          pageSize: 500,
+          sortField: 'liquidity',
+        }),
+      })
+
+      if (!response.ok) {
+        throw new Error(`HTTP error! status: ${response.status}`)
+      }
+
+      const result: ApiResponse = await response.json()
+      if (result.result) {
+        const { records } = result.result.data
+        const childData = records.map((item, index) => ({
+          key: `${parentPositionAddress}-${item.id || index}`,
+          ...item,
+        }))
+        setChildTableData((prev) => ({
+          ...prev,
+          [parentPositionAddress]: childData,
+        }))
+      }
+    } catch (error) {
+      console.error('Error fetching child data:', error)
+      message.error('获取子表格数据失败')
+    } finally {
+      setChildTableLoading((prev) => ({
+        ...prev,
+        [parentPositionAddress]: false,
+      }))
+    }
+  }
+
+  // 处理展开/收起
+  const handleExpand = (expanded: boolean, record: TableData) => {
+    const positionAddress = record.positionAddress as string
+    if (expanded && positionAddress) {
+      setExpandedRowKeys((prev) => [...prev, record.key])
+      fetchChildData(positionAddress)
+    } else {
+      setExpandedRowKeys((prev) =>
+        prev.filter((key) => key !== record.key)
+      )
+    }
+  }
+
+  const handleRefresh = () => {
+    fetchData(1, 100, selectedPoolAddress, sortField)
+  }
+
+  // 子表格的列定义
+  const childColumns = [
+    {
+      title: '创建地址',
+      dataIndex: 'walletAddress',
+      key: 'walletAddress',
+      render: (text: string) => {
+        return (
+          <span className="font-bold text-base">
+            {text.slice(0, 6)}...{text.slice(-4)}
+          </span>
+        )
+      },
+    },
+    {
+      title: 'Liquidity',
+      dataIndex: 'liquidityUsd',
+      key: 'liquidityUsd',
+      render: (text: string) => {
+        return (
+          <span
+            className="text-orange-500 font-bold text-base"
+            style={{ color: '#00B098' }}
+          >
+            ${Number(text).toFixed(2)}
+          </span>
+        )
+      },
+    },
+    {
+      title: '复制数',
+      dataIndex: 'copies',
+      key: 'copies',
+      render: (text: string) => {
+        return <span className="text-green-500 font-bold text-base">{text}</span>
+      },
+    },
+    {
+      title: '奖励',
+      dataIndex: 'bonusUsd',
+      key: 'bonusUsd',
+      render: (text: string) => {
+        return (
+          <span className="text-orange-500 font-bold text-base">
+            ${Number(text).toFixed(2)}
+          </span>
+        )
+      },
+    },
+    {
+      title: 'FEE',
+      dataIndex: 'earnedUsd',
+      key: 'earnedUsd',
+      render: (text: string) => {
+        return (
+          <span
+            className="text-orange-500 font-bold text-base"
+            style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000' }}
+          >
+            ${Number(text).toFixed(2)}
+          </span>
+        )
+      },
+    },
+    {
+      title: 'PNL',
+      dataIndex: 'pnlUsd',
+      key: 'pnlUsd',
+      render: (text: string) => {
+        return (
+          <span
+            className="text-orange-500 font-bold text-base"
+            style={{ color: Number(text) > 0 ? '#00B098' : '#FF0000' }}
+          >
+            ${Number(text).toFixed(2)}
+          </span>
+        )
+      },
+    },
+    {
+      title: '创建时间',
+      dataIndex: 'positionAgeMs',
+      key: 'positionAgeMs',
+      render: (text: string) => {
+        return (
+          <span className="font-bold text-base">
+            {Math.floor(Number(text) / 1000 / 60 / 60)} 小时
+          </span>
+        )
+      },
+    },
+    {
+      title: '状态',
+      dataIndex: 'status',
+      key: 'status',
+      render: (status: number) => {
+        return status === 0 ? (
+          <Tag color="green">Active</Tag>
+        ) : (
+          <Tag color="red">Inactive</Tag>
+        )
+      },
+    },
+    {
+      title: '操作',
+      dataIndex: 'walletAddress',
+      key: 'walletAddress',
+      render: (text: string, record: TableData) => {
+        return (
+          <Typography.Link
+            href={`https://www.byreal.io/en/portfolio?userAddress=${text}&tab=current&positionAddress=${record.positionAddress}`}
+            target="_blank"
+          >
+            复制
+          </Typography.Link>
+        )
+      },
+    },
+  ]
+
+  return (
+    <div style={{ padding: '24px' }}>
+      <div style={{ marginBottom: '16px' }}>
+        <Select
+          style={{ width: 300 }}
+          placeholder="选择 Pool"
+          value={selectedPoolAddress}
+          onChange={handlePoolChange}
+          options={poolOptions}
+          showSearch
+          filterOption={(input, option) =>
+            (option?.label ?? '').toLowerCase().includes(input.toLowerCase())
+          }
+        />
+        <Button type="primary" className="ml-2"  onClick={handleRefresh}>刷新</Button>
+      </div>
+      <Table
+        columns={columns}
+        dataSource={data}
+        loading={loading}
+        pagination={{
+          ...pagination,
+          showSizeChanger: true,
+          showTotal: (total) => `共 ${total} 条`,
+        }}
+        onChange={handleTableChange}
+        scroll={{ x: 'max-content' }}
+        expandable={{
+          expandedRowKeys,
+          onExpand: handleExpand,
+          expandedRowRender: (record: TableData) => {
+            const positionAddress = record.positionAddress as string
+            const childData = childTableData[positionAddress] || []
+            const isLoading = childTableLoading[positionAddress] || false
+
+            return (
+              <Table
+                columns={childColumns}
+                dataSource={childData}
+                loading={isLoading}
+                pagination={false}
+                size="small"
+                scroll={{ x: 'max-content' }}
+              />
+            )
+          },
+        }}
+      />
+    </div>
+  )
+}

+ 14 - 14
src/app/layout.tsx

@@ -1,26 +1,26 @@
-import type { Metadata } from "next";
-import { Geist, Geist_Mono } from "next/font/google";
-import "./globals.css";
+import type { Metadata } from 'next'
+import { Geist, Geist_Mono } from 'next/font/google'
+import './globals.css'
 
 const geistSans = Geist({
-  variable: "--font-geist-sans",
-  subsets: ["latin"],
-});
+  variable: '--font-geist-sans',
+  subsets: ['latin'],
+})
 
 const geistMono = Geist_Mono({
-  variable: "--font-geist-mono",
-  subsets: ["latin"],
-});
+  variable: '--font-geist-mono',
+  subsets: ['latin'],
+})
 
 export const metadata: Metadata = {
-  title: "Create Next App",
-  description: "Generated by create next app",
-};
+  title: 'Byreal Table',
+  description: 'Byreal Table - Top Positions Dashboard',
+}
 
 export default function RootLayout({
   children,
 }: Readonly<{
-  children: React.ReactNode;
+  children: React.ReactNode
 }>) {
   return (
     <html lang="en">
@@ -30,5 +30,5 @@ export default function RootLayout({
         {children}
       </body>
     </html>
-  );
+  )
 }

+ 5 - 61
src/app/page.tsx

@@ -1,65 +1,9 @@
-import Image from "next/image";
+import DataTable from './components/DataTable'
 
 export default function Home() {
   return (
-    <div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
-      <main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
-        <Image
-          className="dark:invert"
-          src="/next.svg"
-          alt="Next.js logo"
-          width={100}
-          height={20}
-          priority
-        />
-        <div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
-          <h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
-            To get started, edit the page.tsx file.
-          </h1>
-          <p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
-            Looking for a starting point or more instructions? Head over to{" "}
-            <a
-              href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-              className="font-medium text-zinc-950 dark:text-zinc-50"
-            >
-              Templates
-            </a>{" "}
-            or the{" "}
-            <a
-              href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-              className="font-medium text-zinc-950 dark:text-zinc-50"
-            >
-              Learning
-            </a>{" "}
-            center.
-          </p>
-        </div>
-        <div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
-          <a
-            className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
-            href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-            target="_blank"
-            rel="noopener noreferrer"
-          >
-            <Image
-              className="dark:invert"
-              src="/vercel.svg"
-              alt="Vercel logomark"
-              width={16}
-              height={16}
-            />
-            Deploy Now
-          </a>
-          <a
-            className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
-            href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
-            target="_blank"
-            rel="noopener noreferrer"
-          >
-            Documentation
-          </a>
-        </div>
-      </main>
-    </div>
-  );
+    <main>
+      <DataTable />
+    </main>
+  )
 }

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů