port webapp to vite

This commit is contained in:
ppc
2024-08-10 10:21:09 +01:00
parent 66908ba601
commit eb80267146
62 changed files with 2074 additions and 3453 deletions
+19 -306
View File
@@ -49,11 +49,6 @@ Generated\ Files/
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
@@ -62,9 +57,6 @@ project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
@@ -89,7 +81,6 @@ StyleCopReport.xml
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
@@ -98,20 +89,6 @@ StyleCopReport.xml
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
@@ -121,314 +98,50 @@ ipch/
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
# ReSharper
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
# dependencies
node_modules
.pnp
.pnp.js
.yarn/install-state.gz
# testing
coverage
# next.js
.next/
out/
# production
build
# misc
.DS_Store
*.pem
# debug
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# local env files
.env*.local
node_modules
dist
dist-ssr
*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.ntvs*
*.njsproj
*.sln
*.sw?
-9
View File
@@ -1,9 +0,0 @@
Dockerfile
.dockerignore
node_modules
npm-debug.log
README.md
.next
!.next/static
!.next/standalone
.git
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}
-56
View File
@@ -1,56 +0,0 @@
FROM node:lts-alpine AS base
# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app
# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
ENV PORT=3000
CMD HOSTNAME="0.0.0.0" node server.js
-41
View File
@@ -1,41 +0,0 @@
import type {Metadata, Viewport} from "next";
import {ThemeProvider} from "@/components/theme-provider";
import "./globals.css";
export const metadata: Metadata = {
title: "AimeNet",
applicationName: "AimeNet",
authors: [
{
name: "ppc",
}
],
openGraph: {
type: 'website',
locale: 'en_US',
siteName: 'AimeNet',
description: 'Browser-local Aime/Amusement IC Card Switcher'
}
};
export const viewport: Viewport = {
initialScale: 1,
minimumScale: 1,
maximumScale: 1,
userScalable: false,
width: 'device-width',
themeColor: '#d2005b',
}
export default function RootLayout({children}: Readonly<{ children: React.ReactNode; }>) {
return (
<html lang="en" suppressHydrationWarning>
<head></head>
<body className="min-h-screen bg-background">
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
{children}
</ThemeProvider>
</body>
</html>
);
}
-39
View File
@@ -1,39 +0,0 @@
import {MetadataRoute} from 'next'
export default function manifest(): MetadataRoute.Manifest {
return {
id: '?pwa-home=1',
name: 'AimeNet',
description: 'Aime Card Switcher',
start_url: '/',
categories: ['utilities', 'games', 'entertainment'],
orientation: 'portrait-primary',
display: 'standalone',
background_color: '#140606',
theme_color: '#d2005b',
icons: [
{
"src": "/icon2.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon3.png",
"sizes": "512x512",
"type": "image/png"
}
],
shortcuts: [
{
url: '/',
name: 'Servers',
description: 'View, add or connect to servers'
},
{
url: '/cards',
name: 'Cards',
description: 'Manage stored cards'
}
]
}
}
-10
View File
@@ -1,10 +0,0 @@
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
disallow: '/'
}
}
}
+3 -3
View File
@@ -1,11 +1,11 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "app/globals.css",
"config": "tailwind.config.js",
"css": "src/index.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
@@ -1,9 +0,0 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}
+26
View File
@@ -0,0 +1,26 @@
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
export default tseslint.config({
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'],
ignores: ['dist', 'src/components/ui'],
languageOptions: {
ecmaVersion: 2021,
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: {
...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [
'warn',
{allowConstantExport: true},
],
},
})
+40
View File
@@ -0,0 +1,40 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="theme-color" content="#d2005b">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<title>AimeNet</title>
<meta name="author" content="ppc">
<meta name="application-name" content="AimeNet">
<meta property="og:locale" content="en_US">
<meta property="og:type" content="website">
<meta property="og:title" content="AimeNet">
<meta property="og:site_name" content="AimeNet">
<meta property="og:description" content="Browser-local Aime/Amusement IC Card Switcher">
<meta name="twitter:card" content="summary">
<meta name="twitter:title" content="AimeNet">
<meta name="twitter:description" content="Browser-local Aime/Amusement IC Card Switcher">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="AimeNet">
<link rel="icon" href="/favicon.ico" type="image/x-icon" sizes="48x48">
<link rel="icon" href="/icon-16.png" type="image/png" sizes="16x16">
<link rel="icon" href="/icon-32.png" type="image/png" sizes="32x32">
<link rel="icon" href="/icon-192.png" type="image/png" sizes="192x192">
<link rel="icon" href="/icon-512.png" type="image/png" sizes="512x512">
<link rel="apple-touch-icon" href="/apple-icon.png" type="image/png" sizes="180x180">
<link rel="manifest" href="/app.webmanifest" crossorigin="use-credentials">
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
-6
View File
@@ -1,6 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
};
export default nextConfig;
+1470 -2673
View File
File diff suppressed because it is too large Load Diff
+26 -21
View File
@@ -1,35 +1,33 @@
{
"name": "aimenet-web-2",
"version": "0.1.0",
"name": "aimenet-web",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@hookform/resolvers": "^3.9.0",
"@radix-ui/react-context-menu": "^2.2.1",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-dropdown-menu": "^2.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-separator": "^1.1.0",
"@radix-ui/react-slot": "^1.1.0",
"@radix-ui/react-toast": "^1.2.1",
"@radix-ui/react-tooltip": "^1.1.2",
"@uidotdev/usehooks": "^2.4.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"detectincognitojs": "^1.3.5",
"dexie": "^4.0.8",
"dexie-react-hooks": "^1.1.7",
"lucide-react": "^0.424.0",
"next": "14.2.5",
"next-themes": "^0.3.0",
"react": "^18",
"react-dom": "^18",
"lucide-react": "^0.427.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-hook-form": "^7.52.2",
"react-router-dom": "^6.26.0",
"react-timeago": "^7.2.0",
"tailwind-merge": "^2.4.0",
"tailwindcss-animate": "^1.0.7",
@@ -37,14 +35,21 @@
"zod": "^3.23.8"
},
"devDependencies": {
"@types/node": "^20",
"@types/react": "^18",
"@types/react-dom": "^18",
"@eslint/js": "^9.8.0",
"@types/node": "^22.2.0",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-timeago": "^4.1.7",
"eslint": "^8",
"eslint-config-next": "14.2.5",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
"@vitejs/plugin-react-swc": "^3.5.0",
"autoprefixer": "^10.4.20",
"eslint": "^9.8.0",
"eslint-plugin-react-hooks": "^5.1.0-rc.0",
"eslint-plugin-react-refresh": "^0.4.9",
"globals": "^15.9.0",
"postcss": "^8.4.41",
"tailwindcss": "^3.4.9",
"typescript": "^5.5.3",
"typescript-eslint": "^8.0.0",
"vite": "^5.4.0"
}
}
+6
View File
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
-8
View File
@@ -1,8 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {
tailwindcss: {},
},
};
export default config;
+39
View File
@@ -0,0 +1,39 @@
{
"id": "?pwa-home=1",
"name": "AimeNet",
"description": "Aime Card Switcher",
"start_url": "/",
"categories": [
"utilities",
"games",
"entertainment"
],
"orientation": "portrait-primary",
"display": "standalone",
"background_color": "#140606",
"theme_color": "#d2005b",
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"shortcuts": [
{
"url": "/",
"name": "Servers",
"description": "View, add or connect to servers"
},
{
"url": "/cards",
"name": "Cards",
"description": "Manage stored cards"
}
]
}

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Before

Width:  |  Height:  |  Size: 3.0 KiB

After

Width:  |  Height:  |  Size: 3.0 KiB

Before

Width:  |  Height:  |  Size: 343 KiB

After

Width:  |  Height:  |  Size: 343 KiB

+2
View File
@@ -0,0 +1,2 @@
User-Agent: *
Disallow: /
@@ -1,8 +1,9 @@
import {CreditCard, History} from "lucide-react";
import {Card} from "@/components/ui/card";
import React from "react";
import {AimeCard} from "@/lib/database";
import TimeAgo from "react-timeago";
import {CreditCard, History} from "lucide-react";
import {AimeCard} from "@/lib/database";
import {Card} from "@/components/ui/card";
export function AimeCardEntry(props: { card: AimeCard, children?: Readonly<React.ReactNode> }) {
return (
@@ -1,15 +1,24 @@
"use client";
import {z} from "zod";
import React from "react";
import {Form, FormProvider, useForm} from "react-hook-form";
import {FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
import {generateAimeCardId, validateAimeCardId} from "@/lib/aime-card";
import {Check, X} from "lucide-react";
import {FormProvider, useForm} from "react-hook-form";
import {useMediaQuery} from "@/hooks/use-media-query";
import {zodResolver} from "@hookform/resolvers/zod";
import {AimeCard, db} from "@/lib/database";
import {generateAimeCardId, validateAimeCardId} from "@/lib/aime-card";
import {Button} from "@/components/ui/button";
import {Input} from "@/components/ui/input";
import {AimeCard, db} from "@/lib/database";
import {
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/ui/form";
import {
Dialog,
DialogContent,
@@ -18,14 +27,15 @@ import {
DialogTrigger
} from "@/components/ui/dialog";
import {
Drawer, DrawerClose,
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription, DrawerFooter,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger
} from "@/components/ui/drawer";
import {Check, X} from "lucide-react";
const schema = z.object({
name: z.string().min(1, {
@@ -36,53 +46,6 @@ const schema = z.object({
})
});
export function CardCreationDialog(props: { children: Readonly<React.ReactNode> }) {
const [open, setOpen] = React.useState(false);
const isDesktop = useMediaQuery("(min-width: 768px)");
if (isDesktop) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{props.children}
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Add new card</DialogTitle>
</DialogHeader>
<AimeCardCreationForm callback={() => setOpen(false)}/>
</DialogContent>
</Dialog>
);
}
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
{props.children}
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="text-left">
<DrawerTitle>Add card</DrawerTitle>
<DrawerDescription>
Enter the card details below, and press submit to add it to your profile.
</DrawerDescription>
</DrawerHeader>
<div className="px-4">
<AimeCardCreationForm callback={() => setOpen(false)}/>
</div>
<DrawerFooter className="pt-2">
<DrawerClose asChild>
<Button variant="outline">
<X className="w-4 h-4 mr-2"/> Cancel
</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
function AimeCardCreationForm(props: { callback?: () => void }) {
const form = useForm<z.infer<typeof schema>>({
resolver: zodResolver(schema),
@@ -150,4 +113,51 @@ function AimeCardCreationForm(props: { callback?: () => void }) {
</form>
</FormProvider>
);
}
export function CardCreationDialog(props: { children: Readonly<React.ReactNode> }) {
const [open, setOpen] = React.useState(false);
const isDesktop = useMediaQuery("(min-width: 768px)");
if (isDesktop) {
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
{props.children}
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Add new card</DialogTitle>
</DialogHeader>
<AimeCardCreationForm callback={() => setOpen(false)}/>
</DialogContent>
</Dialog>
);
}
return (
<Drawer open={open} onOpenChange={setOpen}>
<DrawerTrigger asChild>
{props.children}
</DrawerTrigger>
<DrawerContent>
<DrawerHeader className="text-left">
<DrawerTitle>Add card</DrawerTitle>
<DrawerDescription>
Enter the card details below, and press submit to add it to your profile.
</DrawerDescription>
</DrawerHeader>
<div className="px-4">
<AimeCardCreationForm callback={() => setOpen(false)}/>
</div>
<DrawerFooter className="pt-2">
<DrawerClose asChild>
<Button variant="outline">
<X className="w-4 h-4 mr-2"/> Cancel
</Button>
</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
);
}
@@ -1,6 +1,6 @@
import {ServerOff} from "lucide-react";
import {useEffect, useState} from "react";
import detectIncognito from "detectincognitojs";
import React, {useEffect, useState} from "react";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
export function IncognitoStorageWarning() {
@@ -1,5 +1,4 @@
import {Loader} from "lucide-react";
import React from "react";
export function LoadingIndicator() {
return (
@@ -1,5 +1,4 @@
import {CreditCard} from "lucide-react";
import React from "react";
export function NoCardsNotification() {
return (<div className="flex flex-col gap-2 items-center text-gray-500 pt-6 pb-5">
@@ -0,0 +1,73 @@
import {createContext, useContext, useEffect, useState} from "react"
type Theme = "dark" | "light" | "system"
type ThemeProviderProps = {
children: React.ReactNode
defaultTheme?: Theme
storageKey?: string
}
type ThemeProviderState = {
theme: Theme
setTheme: (theme: Theme) => void
}
const initialState: ThemeProviderState = {
theme: "system",
setTheme: () => null,
}
const ThemeProviderContext = createContext<ThemeProviderState>(initialState)
export function ThemeProvider({
children,
defaultTheme = "system",
storageKey = "vite-ui-theme",
...props
}: ThemeProviderProps) {
const [theme, setTheme] = useState<Theme>(
() => (localStorage.getItem(storageKey) as Theme) || defaultTheme
)
useEffect(() => {
const root = window.document.documentElement
root.classList.remove("light", "dark")
if (theme === "system") {
const systemTheme = window.matchMedia("(prefers-color-scheme: dark)")
.matches
? "dark"
: "light"
root.classList.add(systemTheme)
return
}
root.classList.add(theme)
}, [theme])
const value = {
theme,
setTheme: (theme: Theme) => {
localStorage.setItem(storageKey, theme)
setTheme(theme)
},
}
return (
<ThemeProviderContext.Provider {...props} value={value}>
{children}
</ThemeProviderContext.Provider>
)
}
export const useTheme = () => {
const context = useContext(ThemeProviderContext)
if (context === undefined)
throw new Error("useTheme must be used within a ThemeProvider")
return context
}
@@ -9,8 +9,7 @@ const alertVariants = cva(
variants: {
variant: {
default: "bg-background text-foreground",
destructive:
"border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
destructive: "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive",
blank: "[&>svg]:text-inherit"
},
},
@@ -1,5 +1,3 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"
@@ -1,5 +1,3 @@
"use client"
import * as React from "react"
import { Drawer as DrawerPrimitive } from "vaul"
@@ -1,5 +1,3 @@
"use client"
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
+37
View File
@@ -0,0 +1,37 @@
import './index.css'
import {StrictMode} from 'react'
import {createRoot} from 'react-dom/client'
import {createBrowserRouter, RouterProvider} from "react-router-dom"
import {Toaster} from "@/components/ui/toaster.tsx";
import {ThemeProvider} from "@/components/theme-provider.tsx";
import CardManager from "@/pages/card-manager.tsx"
import ServerCardIn from "@/pages/card-in-screen.tsx"
import ServerListing from "@/pages/server-listing.tsx"
const router = createBrowserRouter([
{
path: '/',
element: <ServerListing/>
},
{
path: '/cards',
element: <CardManager/>
},
{
path: '/servers/:serverId',
element: <ServerCardIn/>
}
])
createRoot(document.getElementById('root')!).render(
<StrictMode>
<ThemeProvider defaultTheme="system" storageKey="vite-ui-theme">
<RouterProvider router={router}/>
<Toaster/>
</ThemeProvider>
</StrictMode>,
)
@@ -1,57 +1,24 @@
"use client";
import {useEffect, useState} from "react";
import {useNavigate, useParams} from "react-router-dom";
import {useLiveQuery} from "dexie-react-hooks";
import React, {useEffect, useState} from "react";
import {useParams, useRouter} from "next/navigation";
import {ChevronRight, Circle, CircleCheckBig, Home, Pencil, Plus, ServerOff, Wallet} from "lucide-react";
import {ChevronRight, Circle, CircleCheckBig, Home, Plus, ServerOff, Wallet} from "lucide-react";
import {AimeCard, AimeServer, db} from "@/lib/database";
import {getServerInfo, serverCardIn} from "@/lib/aime-net";
import {Button} from "@/components/ui/button";
import {Footer} from "@/components/footer";
import {Toaster} from "@/components/ui/toaster";
import {Button} from "@/components/ui/button";
import {useToast} from "@/components/ui/use-toast";
import {Separator} from "@/components/ui/separator";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
import {AimeCardEntry} from "@/components/aime-card";
import {LoadingIndicator} from "@/components/loading-indicator";
import {CardCreationDialog} from "@/components/card-creation-form";
import {NoCardsNotification} from "@/components/no-cards-notification";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
function CardList(props: { cards: ReadonlyArray<AimeCard> | null, cardSelected: (card: AimeCard) => void }) {
const router = useRouter();
return (<>
<h4 className="text-2xl font-bold">{props.cards?.length ? "Cards" : "Additional Cards"}</h4>
<div className="flex gap-2">
<CardCreationDialog>
<Button className="bg-blue-500 flex-grow">
<Plus className="mr-2 h-4 w-4"/> Add Card
</Button>
</CardCreationDialog>
<Button onClick={() => router.push('/cards')} variant="outline" size="icon">
<Wallet className="h-4 w-4"/>
</Button>
</div>
{props.cards?.length ? props.cards.map(x => <AimeCardEntry card={x} key={x.id}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="ghost" onClick={() => props.cardSelected(x)}>
<Circle className="w-5 h-5"/>
</Button>
</TooltipTrigger>
<TooltipContent side="left">
<p>Select Card</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</AimeCardEntry>) : <NoCardsNotification/>}
</>);
}
/* eslint-disable @typescript-eslint/no-explicit-any */
async function performCardInAction(card: AimeCard, server: AimeServer, setCardInStatus: (status: boolean) => void, toaster: any) {
setCardInStatus(true);
@@ -76,12 +43,47 @@ async function performCardInAction(card: AimeCard, server: AimeServer, setCardIn
setTimeout(() => setCardInStatus(false), 1000);
}
}
/* eslint-enable @typescript-eslint/no-explicit-any */
export default function ServerPage() {
const {serverId} = useParams<{ serverId: string }>();
function CardList(props: { cards: ReadonlyArray<AimeCard> | null, cardSelected: (card: AimeCard) => void }) {
const navigate = useNavigate();
return (<>
<h4 className="text-2xl font-bold">{props.cards?.length ? "Cards" : "Additional Cards"}</h4>
<div className="flex gap-2">
<CardCreationDialog>
<Button className="bg-blue-500 flex-grow">
<Plus className="mr-2 h-4 w-4"/> Add Card
</Button>
</CardCreationDialog>
<Button onClick={() => navigate('/cards')} variant="outline" size="icon">
<Wallet className="h-4 w-4"/>
</Button>
</div>
{props.cards?.length ? props.cards.map(x => <AimeCardEntry card={x} key={x.id}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button size="icon" variant="ghost" onClick={() => props.cardSelected(x)}>
<Circle className="w-5 h-5"/>
</Button>
</TooltipTrigger>
<TooltipContent side="left">
<p>Select Card</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</AimeCardEntry>) : <NoCardsNotification/>}
</>);
}
function ServerCardInPage() {
const {toast} = useToast();
const router = useRouter();
const {serverId} = useParams<{ serverId: string }>();
const navigate = useNavigate();
const cards = useLiveQuery(() => db.cards.toArray());
const [server, setServer] = useState<AimeServer | null>(null);
@@ -93,15 +95,20 @@ export default function ServerPage() {
useEffect(() => setSelectedCard(cards?.sort(x => x.lastUsed)[0]), [cards]);
useEffect(() => {
const loadServer = async () => {
if (!serverId) {
navigate('/');
return;
}
const sid = parseInt(serverId);
if (isNaN(sid)) {
router.push('/');
navigate('/');
return;
}
const server = await db.servers.get(sid);
if (!server) {
router.push('/');
navigate('/');
return;
}
@@ -128,7 +135,7 @@ export default function ServerPage() {
<main className="flex flex-col gap-5 container min-h-screen py-10">
<div className="flex items-center gap-1">
<h2 className="text-4xl font-bold select-none flex-grow">{server?.name ?? "Server"}</h2>
<Button variant="ghost" onClick={() => router.push('/')}>
<Button variant="ghost" onClick={() => navigate('/')}>
<Home className="h-4 w-4 sm:mr-2"/>
<span className="hidden sm:block">Servers</span>
</Button>
@@ -177,8 +184,8 @@ export default function ServerPage() {
)}
<Footer/>
</main>
<Toaster/>
</>
);
}
}
export default ServerCardInPage;
@@ -1,22 +1,22 @@
"use client";
import {useNavigate} from "react-router-dom";
import {useLiveQuery} from "dexie-react-hooks";
import {Plus, Server, Trash} from "lucide-react";
import {AimeCard, db} from "@/lib/database";
import React from "react";
import {Separator} from "@/components/ui/separator";
import {Footer} from "@/components/footer";
import {Button} from "@/components/ui/button";
import {Plus, Server, Trash} from "lucide-react";
import {useRouter} from "next/navigation";
import {useLiveQuery} from "dexie-react-hooks";
import {AimeCard, db} from "@/lib/database";
import {LoadingIndicator} from "@/components/loading-indicator";
import {CardCreationDialog} from "@/components/card-creation-form";
import {Separator} from "@/components/ui/separator";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger
} from "@/components/ui/context-menu";
import {AimeCardEntry} from "@/components/aime-card";
import {LoadingIndicator} from "@/components/loading-indicator";
import {CardCreationDialog} from "@/components/card-creation-form";
import {IncognitoStorageWarning} from "@/components/incognito-warning";
import {NoCardsNotification} from "@/components/no-cards-notification";
@@ -56,15 +56,15 @@ function CardList(props: { cards: AimeCard[] | undefined }) {
</>);
}
export default function CardManagerPage() {
const router = useRouter();
function CardManagerPage() {
const navigate = useNavigate();
const cards = useLiveQuery(() => db.cards.toArray());
return (
<main className="flex flex-col gap-5 container min-h-screen py-10">
<div className="flex items-center gap-1">
<h2 className="text-4xl font-bold select-none flex-grow">Cards</h2>
<Button variant="ghost" size={"default"} onClick={() => router.push('/')}>
<Button variant="ghost" size={"default"} onClick={() => navigate('/')}>
<Server className="h-4 w-4 sm:mr-2"/>
<span className="hidden sm:block">Manage Servers</span>
</Button>
@@ -78,4 +78,6 @@ export default function CardManagerPage() {
<Footer/>
</main>
);
}
}
export default CardManagerPage;
@@ -1,12 +1,10 @@
"use client";
import {z} from "zod";
import Link from "next/link";
import React, {useState} from "react";
import TimeAgo from "react-timeago";
import React, {useState} from "react";
import {useForm} from "react-hook-form";
import {useRouter} from "next/navigation";
import {AppRouterInstance} from "next/dist/shared/lib/app-router-context.shared-runtime";
import {useNavigate} from 'react-router-dom';
import {useLiveQuery} from "dexie-react-hooks";
import {zodResolver} from "@hookform/resolvers/zod";
import {
ChevronRight,
Earth,
@@ -16,14 +14,17 @@ import {
SquareArrowOutUpRight,
Trash, WalletCards
} from "lucide-react";
import {useLiveQuery} from "dexie-react-hooks";
import {AimeServer, db} from "@/lib/database";
import {zodResolver} from "@hookform/resolvers/zod";
import {Card} from "@/components/ui/card";
import {Footer} from "@/components/footer";
import {Input} from "@/components/ui/input";
import {Button} from "@/components/ui/button";
import {Footer} from "@/components/footer";
import {Separator} from "@/components/ui/separator";
import {LoadingIndicator} from "@/components/loading-indicator";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {Form, FormControl, FormField, FormItem, FormMessage} from "@/components/ui/form";
import {
ContextMenu,
ContextMenuContent,
@@ -31,20 +32,13 @@ import {
ContextMenuSeparator,
ContextMenuTrigger
} from "@/components/ui/context-menu";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {Form, FormControl, FormField, FormItem, FormMessage} from "@/components/ui/form";
import {LoadingIndicator} from "@/components/loading-indicator";
const serverAdditionSchema = z.object({
address: z.string().startsWith("http")
});
function redirectToServerPage(router: AppRouterInstance, server: AimeServer) {
router.push(`/servers/${server.id}`);
}
function ServerList(props: { servers: AimeServer[] | undefined }) {
const router = useRouter();
const navigate = useNavigate();
if (!props.servers) {
return (<LoadingIndicator/>);
@@ -83,14 +77,14 @@ function ServerList(props: { servers: AimeServer[] | undefined }) {
</div>
</div>
</div>
<Button variant="ghost" size="icon" onClick={() => redirectToServerPage(router, s)}>
<Button variant="ghost" size="icon" onClick={() => navigate(`/servers/${s.id}`)}>
<ChevronRight className="h-4 w-4"/>
</Button>
</div>
</Card>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem inset onClick={() => redirectToServerPage(router, s)}>
<ContextMenuItem inset onClick={() => navigate(`/servers/${s.id}`)}>
Select Server
</ContextMenuItem>
<ContextMenuSeparator/>
@@ -105,7 +99,7 @@ function ServerList(props: { servers: AimeServer[] | undefined }) {
}
function ServerAddForm() {
const router = useRouter();
const navigate = useNavigate();
const form = useForm<z.infer<typeof serverAdditionSchema>>({
resolver: zodResolver(serverAdditionSchema),
defaultValues: {
@@ -136,9 +130,9 @@ function ServerAddForm() {
name: `Server #${newId}`
});
router.push(`/servers/${newId}`);
navigate(`/servers/${newId}`);
} else {
router.push(`/servers/${existingServer.id}`);
navigate(`/servers/${existingServer.id}`);
}
}
@@ -165,11 +159,12 @@ function ServerAddForm() {
<AlertTitle>HTTPS Warning</AlertTitle>
<AlertDescription>
AimeNet doesn&apos;t support HTTPS out-the-box.
If you&apos;re using HTTPS, ensure a reverse-proxy or <Link target="_blank"
className="underline"
href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/httpsys">
HTTP.SYS <SquareArrowOutUpRight className="inline h-3 w-3"/>
</Link> has been configured and a valid, trusted SSL certificate has been configured.
If you&apos;re using HTTPS, ensure a reverse-proxy or
<a target="_blank"
className="underline"
href="https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/httpsys">
HTTP.SYS <SquareArrowOutUpRight className="inline h-3 w-3"/>
</a> has been configured and a valid, trusted SSL certificate has been configured.
</AlertDescription>
</Alert>
</>
@@ -201,15 +196,15 @@ function ServerAddForm() {
);
}
export default function ServerListingPage() {
function ServerListing() {
const navigate = useNavigate();
const servers = useLiveQuery(() => db.servers.toArray());
const router = useRouter();
return (
<main className="flex flex-col gap-5 container min-h-screen py-10">
<div className="flex items-center gap-1">
<h2 className="text-4xl font-bold select-none flex-grow">Servers</h2>
<Button variant="ghost" size={"default"} onClick={() => router.push('/cards')}>
<Button variant="ghost" size={"default"} onClick={() => navigate('/cards')}>
<WalletCards className="h-4 w-4 sm:mr-2"/>
<span className="hidden sm:block">Manage Cards</span>
</Button>
@@ -224,3 +219,5 @@ export default function ServerListingPage() {
</main>
);
}
export default ServerListing;
+1
View File
@@ -0,0 +1 @@
/// <reference types="vite/client" />
+74
View File
@@ -0,0 +1,74 @@
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: ["class"],
content: [
'./src/**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
}
-81
View File
@@ -1,81 +0,0 @@
import type {Config} from "tailwindcss"
import {fontFamily} from "tailwindcss/defaultTheme"
const config = {
darkMode: ["class"],
content: [
'**/*.{ts,tsx}',
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
fontFamily: {
sans: ["system-ui"],
},
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: {height: "0"},
to: {height: "var(--radix-accordion-content-height)"},
},
"accordion-up": {
from: {height: "var(--radix-accordion-content-height)"},
to: {height: "0"},
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config
export default config
+34
View File
@@ -0,0 +1,34 @@
{
"compilerOptions": {
"target": "ES2021",
"useDefineForClassFields": true,
"lib": [
"ES2021",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
},
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": [
"src"
]
}
+16 -23
View File
@@ -1,26 +1,19 @@
{
"compilerOptions": {
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./*"]
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
}
}
+22
View File
@@ -0,0 +1,22 @@
{
"compilerOptions": {
"target": "ES2022",
"lib": ["ES2023"],
"module": "ESNext",
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["vite.config.ts"]
}
+13
View File
@@ -0,0 +1,13 @@
import path from 'path';
import {defineConfig} from 'vite'
import react from '@vitejs/plugin-react-swc'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
},
},
})