commit 6575add5df037f5f73098a851bb014247e1b43ef Author: TZGyn Date: Mon Jan 6 20:23:45 2025 +0800 init diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..3722418 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": ["next/core-web-vitals", "next/typescript"] +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fd3dbb5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,36 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local + +# vercel +.vercel + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..e215bc4 --- /dev/null +++ b/README.md @@ -0,0 +1,36 @@ +This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. + +This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. diff --git a/app/(charts)/day.tsx b/app/(charts)/day.tsx new file mode 100644 index 0000000..e4c2984 --- /dev/null +++ b/app/(charts)/day.tsx @@ -0,0 +1,112 @@ +"use client"; + +import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart"; + +const chartConfig = { + value: { + label: "Count", + color: "hsl(var(--chart-1))", + }, + foldtime: { + label: "Fold Time", + color: "hsl(var(--chart-2))", + }, +} satisfies ChartConfig; + +export function DayChart({ + data, +}: { + data: { + timestamp: number; + foldtime: number; + }[]; +}) { + const thisMonthTimestamp = new Date(); + thisMonthTimestamp.setHours(0, 0, 0, 0); + thisMonthTimestamp.setDate(1); + const nextMonthTimestamp = new Date(); + nextMonthTimestamp.setHours(0, 0, 0, 0); + nextMonthTimestamp.setDate(1); + nextMonthTimestamp.setMonth(nextMonthTimestamp.getMonth() + 1); + + console.log(thisMonthTimestamp.getTime(), nextMonthTimestamp.getTime()); + + const thisMonthData = data.filter( + (data) => + data.timestamp >= thisMonthTimestamp.getTime() && + data.timestamp < nextMonthTimestamp.getTime() + ); + + const todayDataPerHour = thisMonthData.reduce((acc, data) => { + const day = new Date(data.timestamp).getDate(); + if (!acc[day]) { + acc[day] = { + count: 1, + foldTime: data.foldtime, + }; + } + acc[day].count += 1; + acc[day].foldTime += data.foldtime; + return acc; + }, {} as Record); + + const thisMonthDays = new Date(); + thisMonthDays.setMonth(thisMonthDays.getMonth() + 1); + thisMonthDays.setDate(1); + thisMonthDays.setDate(thisMonthDays.getDate() - 1); + + const chartData = new Array(thisMonthDays.getDate()).fill(0).map((_, i) => { + const day = i + 1; + if (!todayDataPerHour[day]) + return { day: "Day " + day, value: 0, foldtime: 0 }; + return { + day: "Day " + day, + value: todayDataPerHour[day].count, + foldtime: + todayDataPerHour[day].foldTime / todayDataPerHour[day].count, + }; + }); + return ( + + + This Month + {/* January - June 2024 */} + + + + + + + } + /> + + + + + + + ); +} diff --git a/app/(charts)/hour.tsx b/app/(charts)/hour.tsx new file mode 100644 index 0000000..b338265 --- /dev/null +++ b/app/(charts)/hour.tsx @@ -0,0 +1,103 @@ +"use client"; + +import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart"; + +const chartConfig = { + value: { + label: "Count", + color: "hsl(var(--chart-1))", + }, + foldtime: { + label: "Fold Time", + color: "hsl(var(--chart-2))", + }, +} satisfies ChartConfig; + +export function HourChart({ + data, +}: { + data: { + timestamp: number; + foldtime: number; + }[]; +}) { + const todayTimestamp = new Date(); + todayTimestamp.setHours(0, 0, 0, 0); + const tomorrowTimestamp = new Date(); + tomorrowTimestamp.setHours(0, 0, 0, 0); + tomorrowTimestamp.setDate(tomorrowTimestamp.getDate() + 1); + + console.log(todayTimestamp.getTime(), tomorrowTimestamp.getTime()); + + const todayData = data.filter( + (data) => + data.timestamp >= todayTimestamp.getTime() && + data.timestamp < tomorrowTimestamp.getTime() + ); + + const todayDataPerHour = todayData.reduce((acc, data) => { + const hour = new Date(data.timestamp).getHours(); + if (!acc[hour]) { + acc[hour] = { + count: 1, + foldTime: data.foldtime, + }; + } + acc[hour].count += 1; + acc[hour].foldTime += data.foldtime; + return acc; + }, {} as Record); + + const chartData = new Array(24).fill(0).map((_, i) => { + if (!todayDataPerHour[i]) + return { hour: i + 1 + ":00", value: 0, foldtime: 0 }; + return { + hour: i + 1 + ":00", + value: todayDataPerHour[i].count, + foldtime: todayDataPerHour[i].foldTime / todayDataPerHour[i].count, + }; + }); + return ( + + + Today + {/* January - June 2024 */} + + + + + + + } + /> + + + + + + + ); +} diff --git a/app/(charts)/minute.tsx b/app/(charts)/minute.tsx new file mode 100644 index 0000000..bcbab3b --- /dev/null +++ b/app/(charts)/minute.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { Bar, BarChart, CartesianGrid, XAxis } from "recharts"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { + ChartConfig, + ChartContainer, + ChartTooltip, + ChartTooltipContent, +} from "@/components/ui/chart"; + +const chartConfig = { + value: { + label: "Count", + color: "hsl(var(--chart-1))", + }, + foldtime: { + label: "Fold Time", + color: "hsl(var(--chart-2))", + }, +} satisfies ChartConfig; + +export function MinuteChart({ + data, +}: { + data: { + timestamp: number; + foldtime: number; + }[]; +}) { + const thisHourTimestamp = new Date(); + thisHourTimestamp.setMinutes(0, 0, 0); + const nextHourTimestamp = new Date(); + nextHourTimestamp.setHours(nextHourTimestamp.getHours() + 1); + nextHourTimestamp.setMinutes(0, 0, 0); + + console.log(thisHourTimestamp.getTime(), nextHourTimestamp.getTime()); + + const todayData = data.filter( + (data) => + data.timestamp >= thisHourTimestamp.getTime() && + data.timestamp < nextHourTimestamp.getTime() + ); + + const todayDataPerHour = todayData.reduce((acc, data) => { + const hour = new Date(data.timestamp).getMinutes(); + if (!acc[hour]) { + acc[hour] = { + count: 1, + foldTime: data.foldtime, + }; + } + acc[hour].count += 1; + acc[hour].foldTime += data.foldtime; + return acc; + }, {} as Record); + + const chartData = new Array(60).fill(0).map((_, i) => { + if (!todayDataPerHour[i]) + return { minute: "Minute " + i, value: 0, foldtime: 0 }; + return { + minute: "Minute " + i, + value: todayDataPerHour[i].count, + foldtime: todayDataPerHour[i].foldTime / todayDataPerHour[i].count, + }; + }); + return ( + + + This Hour + {/* January - June 2024 */} + + + + + + + value.replace("Minute ", "") + } + /> + } + /> + + + + + + + ); +} diff --git a/app/favicon.ico b/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/app/favicon.ico differ diff --git a/app/fonts/GeistMonoVF.woff b/app/fonts/GeistMonoVF.woff new file mode 100644 index 0000000..f2ae185 Binary files /dev/null and b/app/fonts/GeistMonoVF.woff differ diff --git a/app/fonts/GeistVF.woff b/app/fonts/GeistVF.woff new file mode 100644 index 0000000..1b62daa Binary files /dev/null and b/app/fonts/GeistVF.woff differ diff --git a/app/globals.css b/app/globals.css new file mode 100644 index 0000000..4b28afe --- /dev/null +++ b/app/globals.css @@ -0,0 +1,78 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +body { + font-family: Arial, Helvetica, sans-serif; +} + +@layer utilities { + .text-balance { + text-wrap: balance; + } +} + +@layer base { + :root { + --background: 0 0% 100%; + --foreground: 240 10% 3.9%; + --card: 0 0% 100%; + --card-foreground: 240 10% 3.9%; + --popover: 0 0% 100%; + --popover-foreground: 240 10% 3.9%; + --primary: 240 5.9% 10%; + --primary-foreground: 0 0% 98%; + --secondary: 240 4.8% 95.9%; + --secondary-foreground: 240 5.9% 10%; + --muted: 240 4.8% 95.9%; + --muted-foreground: 240 3.8% 46.1%; + --accent: 240 4.8% 95.9%; + --accent-foreground: 240 5.9% 10%; + --destructive: 0 84.2% 60.2%; + --destructive-foreground: 0 0% 98%; + --border: 240 5.9% 90%; + --input: 240 5.9% 90%; + --ring: 240 10% 3.9%; + --chart-1: 12 76% 61%; + --chart-2: 173 58% 39%; + --chart-3: 197 37% 24%; + --chart-4: 43 74% 66%; + --chart-5: 27 87% 67%; + --radius: 0.5rem; + } + .dark { + --background: 240 10% 3.9%; + --foreground: 0 0% 98%; + --card: 240 10% 3.9%; + --card-foreground: 0 0% 98%; + --popover: 240 10% 3.9%; + --popover-foreground: 0 0% 98%; + --primary: 0 0% 98%; + --primary-foreground: 240 5.9% 10%; + --secondary: 240 3.7% 15.9%; + --secondary-foreground: 0 0% 98%; + --muted: 240 3.7% 15.9%; + --muted-foreground: 240 5% 64.9%; + --accent: 240 3.7% 15.9%; + --accent-foreground: 0 0% 98%; + --destructive: 0 62.8% 30.6%; + --destructive-foreground: 0 0% 98%; + --border: 240 3.7% 15.9%; + --input: 240 3.7% 15.9%; + --ring: 240 4.9% 83.9%; + --chart-1: 220 70% 50%; + --chart-2: 160 60% 45%; + --chart-3: 30 80% 55%; + --chart-4: 280 65% 60%; + --chart-5: 340 75% 55%; + } +} + +@layer base { + * { + @apply border-border; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/app/layout.tsx b/app/layout.tsx new file mode 100644 index 0000000..384b13d --- /dev/null +++ b/app/layout.tsx @@ -0,0 +1,40 @@ +import type { Metadata } from "next"; +import localFont from "next/font/local"; +import "./globals.css"; +import { ScrollArea } from "@/components/ui/scroll-area"; + +const geistSans = localFont({ + src: "./fonts/GeistVF.woff", + variable: "--font-geist-sans", + weight: "100 900", +}); +const geistMono = localFont({ + src: "./fonts/GeistMonoVF.woff", + variable: "--font-geist-mono", + weight: "100 900", +}); + +export const metadata: Metadata = { + title: "Create Next App", + description: "Generated by create next app", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + +
+ {children} +
+
+ + + ); +} diff --git a/app/page.tsx b/app/page.tsx new file mode 100644 index 0000000..51dc3bb --- /dev/null +++ b/app/page.tsx @@ -0,0 +1,28 @@ +import fs from "node:fs"; +import { HourChart } from "./(charts)/hour"; +import { DayChart } from "./(charts)/day"; +import { MinuteChart } from "./(charts)/minute"; + +export default async function Home() { + const file = await fs.promises.readFile("./output.txt"); + + const lines = file.toString().split("\n"); + + const data = lines.map((line) => { + const [timestamp, event, value] = line.split(","); + return { + timestamp: Math.round(parseFloat(timestamp) * 1000), + foldtime: parseFloat(value), + }; + }); + + console.log(data); + + return ( +
+ + + +
+ ); +} diff --git a/components.json b/components.json new file mode 100644 index 0000000..032221d --- /dev/null +++ b/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "default", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "tailwind.config.ts", + "css": "app/globals.css", + "baseColor": "zinc", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/components/ui/card.tsx b/components/ui/card.tsx new file mode 100644 index 0000000..f62edea --- /dev/null +++ b/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/components/ui/chart.tsx b/components/ui/chart.tsx new file mode 100644 index 0000000..32dc873 --- /dev/null +++ b/components/ui/chart.tsx @@ -0,0 +1,365 @@ +"use client" + +import * as React from "react" +import * as RechartsPrimitive from "recharts" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +} + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + + return ( + +
+ + + {children} + +
+
+ ) +}) +ChartContainer.displayName = "Chart" + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([, config]) => config.theme || config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +