Merge remote-tracking branch 'origin/master'

This commit is contained in:
ppc
2024-08-10 16:25:00 +01:00
65 changed files with 2408 additions and 3465 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?
-3
View File
@@ -1,3 +0,0 @@
{
"extends": "next/core-web-vitals"
}
-38
View File
@@ -1,38 +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,
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": ""
-30
View File
@@ -1,30 +0,0 @@
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";
export function AimeCardEntry(props: { card: AimeCard, children?: Readonly<React.ReactNode> }) {
return (
<Card>
<div className="flex p-4 items-center gap-3">
<div className="flex-grow flex flex-col gap-4">
<h4 className="text-2xl font-semibold">{props.card.name}</h4>
<div className="text-md flex gap-3 flex-wrap">
{props.card.lastUsed > 0 && (
<div className="flex items-center gap-2 text-gray-500">
<History className="h-5 w-5"/>
<TimeAgo date={props.card.lastUsed}/>
</div>
)}
<div className="flex items-center gap-2 text-gray-500">
<CreditCard className="h-5 w-5"/>
<code>{props.card.id.match(/.{1,4}/g)?.join(" ")}</code>
</div>
</div>
</div>
{props.children}
</div>
</Card>
);
}
-5
View File
@@ -1,5 +0,0 @@
export function Footer() {
return (<footer>
<span className="text-gray-500 text-sm">AimeNet &copy; ppc {new Date().getUTCFullYear()}</span>
</footer>);
}
@@ -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},
],
},
})
+37
View File
@@ -0,0 +1,37 @@
<!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">
<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>
-4
View File
@@ -1,4 +0,0 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
+1499 -2673
View File
File diff suppressed because it is too large Load Diff
+27 -21
View File
@@ -1,35 +1,34 @@
{
"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-alert-dialog": "^1.1.1",
"@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 +36,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: /
+29
View File
@@ -0,0 +1,29 @@
import React from "react";
import TimeAgo from "react-timeago";
import {CreditCard, History} from "lucide-react";
import {AimeCard} from "@/lib/database";
import {useMediaQuery} from "@/hooks/use-media-query.tsx";
import {AMCard, AMCardActionProps, AMCardDetail} from "@/components/am-card.tsx";
export function AimeCardEntry(props: AMCardActionProps & { card: AimeCard, children?: Readonly<React.ReactNode> }) {
const isDesktop = useMediaQuery("(min-width: 512px)");
return (
<AMCard title={props.card.name} actionIcon={props.actionIcon} actionClicked={props.actionClicked} actionDisabled={props.actionDisabled}>
{props.card.lastUsed > 0 && (
<AMCardDetail>
<History className="h-5 w-5"/>
<TimeAgo date={props.card.lastUsed}/>
</AMCardDetail>
)}
<AMCardDetail>
<CreditCard className="h-5 w-5"/>
<code>{isDesktop
? props.card.id.match(/.{1,4}/g)?.join(" ")
: `•••• ${props.card.id.substring(props.card.id.length - 4)}`}
</code>
</AMCardDetail>
</AMCard>
);
}
+40
View File
@@ -0,0 +1,40 @@
import React from "react";
import {ChevronRight} from "lucide-react";
import {Card} from "@/components/ui/card.tsx";
import {Button} from "@/components/ui/button.tsx";
export interface AMCardActionProps {
actionIcon?: Readonly<React.ReactNode>,
actionClicked?: () => void,
actionDisabled?: boolean,
}
export function AMCard(props: AMCardActionProps & { title: string, children: Readonly<React.ReactNode> }) {
return (
<Card>
<div className="grid grid-cols-[1fr_40px] items-center gap-1 p-4 pr-2 sm:p-6">
<div className="max-w-full overflow-hidden space-y-3">
<h4 className="text-2xl font-semibold select-none text-ellipsis whitespace-nowrap overflow-hidden">
{props.title}
</h4>
<div className="text-sm lg:text-md flex flex-auto flex-wrap gap-3">
{props.children}
</div>
</div>
{props.actionClicked && (
<Button className="flex-shrink-0" variant="ghost" size="icon" disabled={props.actionDisabled} onClick={props.actionClicked}>
{props.actionIcon ?? <ChevronRight className="h-4 w-4"/>}
</Button>
)}
</div>
</Card>
)
}
export function AMCardDetail(props: { children?: Readonly<React.ReactNode> }) {
return (
<div className="inline-flex items-center max-w-full gap-2 text-gray-500">
{props.children}
</div>
)
}
+6
View File
@@ -0,0 +1,6 @@
export function AMFooter(props: {children?: Readonly<React.ReactNode>, className?: string}) {
return (<footer className={props.className ?? "flex sm:justify-start justify-center"}>
<span className="text-gray-500 text-sm text-center">AimeNet &copy; ppc {new Date().getUTCFullYear()}</span>
{props.children}
</footer>);
}
+14
View File
@@ -0,0 +1,14 @@
import React from "react";
import {Separator} from "@/components/ui/separator.tsx";
export function AMHeader(props: { title: string, children?: Readonly<React.ReactNode> }) {
return (
<>
<div className="flex items-center gap-1">
<h2 className="text-4xl font-bold select-none flex-grow">{props.title}</h2>
{props.children}
</div>
<Separator/>
</>
);
}
@@ -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
}
@@ -0,0 +1,139 @@
import * as React from "react"
import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
import { cn } from "@/lib/utils"
import { buttonVariants } from "@/components/ui/button"
const AlertDialog = AlertDialogPrimitive.Root
const AlertDialogTrigger = AlertDialogPrimitive.Trigger
const AlertDialogPortal = AlertDialogPrimitive.Portal
const AlertDialogOverlay = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Overlay
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
ref={ref}
/>
))
AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
const AlertDialogContent = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Content>
>(({ className, ...props }, ref) => (
<AlertDialogPortal>
<AlertDialogOverlay />
<AlertDialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
/>
</AlertDialogPortal>
))
AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
const AlertDialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-2 text-center sm:text-left",
className
)}
{...props}
/>
)
AlertDialogHeader.displayName = "AlertDialogHeader"
const AlertDialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
AlertDialogFooter.displayName = "AlertDialogFooter"
const AlertDialogTitle = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Title
ref={ref}
className={cn("text-lg font-semibold", className)}
{...props}
/>
))
AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
const AlertDialogDescription = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
AlertDialogDescription.displayName =
AlertDialogPrimitive.Description.displayName
const AlertDialogAction = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Action>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Action>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Action
ref={ref}
className={cn(buttonVariants(), className)}
{...props}
/>
))
AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
const AlertDialogCancel = React.forwardRef<
React.ElementRef<typeof AlertDialogPrimitive.Cancel>,
React.ComponentPropsWithoutRef<typeof AlertDialogPrimitive.Cancel>
>(({ className, ...props }, ref) => (
<AlertDialogPrimitive.Cancel
ref={ref}
className={cn(
buttonVariants({ variant: "outline" }),
"mt-2 sm:mt-0",
className
)}
{...props}
/>
))
AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
export {
AlertDialog,
AlertDialogPortal,
AlertDialogOverlay,
AlertDialogTrigger,
AlertDialogContent,
AlertDialogHeader,
AlertDialogFooter,
AlertDialogTitle,
AlertDialogDescription,
AlertDialogAction,
AlertDialogCancel,
}
@@ -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"
@@ -1,5 +1,8 @@
export const AIMENET_API_VERSION = 1;
export interface AimeServerInfo {
name: string;
api_version: number;
}
export interface AimeCardinRequest {
@@ -1,8 +1,9 @@
import Dexie, { type EntityTable } from 'dexie';
interface AimeServer {
import {AimeServerInfo} from "@/lib/aime-net.ts";
interface AimeServer extends AimeServerInfo {
id: number;
name: string;
address: string;
lastConnected: number;
}
+39
View File
@@ -0,0 +1,39 @@
import './index.css'
import {StrictMode} from 'react'
import {createRoot} from 'react-dom/client'
import {createHashRouter, 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"
// iOS PWAs show the navigation bar when changing routes
// HashRouter doesn't have this issue as the URL never changes
const router = createHashRouter([
{
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,35 @@
"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 {useToast} from "@/components/ui/use-toast";
import {Separator} from "@/components/ui/separator";
import {Alert, AlertDescription, AlertTitle} from "@/components/ui/alert";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger
} from "@/components/ui/alert-dialog.tsx";
import {AMHeader} from "@/components/am-header.tsx";
import {AMFooter} from "@/components/am-footer.tsx";
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);
@@ -77,11 +55,38 @@ async function performCardInAction(card: AimeCard, server: AimeServer, setCardIn
}
}
export default function ServerPage() {
/* eslint-enable @typescript-eslint/no-explicit-any */
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
? <NoCardsNotification/>
: props.cards.map(x => <AimeCardEntry key={x.id} card={x}
actionIcon={<Circle className="w-5 h-5"/>}
actionClicked={() => props.cardSelected(x)}/>)}
</>);
}
function ServerCardInPage() {
const {toast} = useToast();
const {serverId} = useParams<{ serverId: string }>();
const {toast} = useToast();
const router = useRouter();
const navigate = useNavigate();
const cards = useLiveQuery(() => db.cards.toArray());
const [server, setServer] = useState<AimeServer | null>(null);
@@ -93,26 +98,29 @@ 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;
}
try {
const info = await getServerInfo(server.address);
await db.servers.update(server.id, info);
server.name = info.name;
server.lastConnected = Date.now();
Object.assign(server, info);
setConnectionError(false);
await db.servers.put(server);
} catch {
setConnectionError(true);
}
@@ -126,22 +134,20 @@ export default function ServerPage() {
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">{server?.name ?? "Server"}</h2>
<Button variant="ghost" onClick={() => router.push('/')}>
<AMHeader title={server?.name ?? "Server"}>
<Button variant="ghost" onClick={() => navigate('/')}>
<Home className="h-4 w-4 sm:mr-2"/>
<span className="hidden sm:block">Servers</span>
</Button>
</div>
<Separator/>
</AMHeader>
{!server && (<LoadingIndicator/>)}
{(server && connectionError) && (<Alert variant="destructive" className="mb-5">
{(server && connectionError) && (<Alert variant="destructive">
<ServerOff className="h-4 w-4"/>
<AlertTitle>Connection Issue</AlertTitle>
<AlertDescription>
There was an issue connecting to the machine at <code>{server.address}</code>. Please check the
server is running and try again.
There was an issue connecting to <code className="max-w-full text-wrap">{server.address}</code>.
Please check the game is running and try again.
</AlertDescription>
</Alert>)}
@@ -175,10 +181,36 @@ export default function ServerPage() {
}}/>
</div>
)}
<Footer/>
</main>
<Toaster/>
<AMFooter className="flex flex-col sm:flex-row justify-center sm:justify-between gap-2 flex-wrap">
{server?.id && (
<AlertDialog>
<AlertDialogTrigger>
<p className="text-sm text-red-700">Delete {server.name}</p>
</AlertDialogTrigger>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Deletion</AlertDialogTitle>
<AlertDialogDescription>
You'll need to re-enter the server information if you want to use it in the future...
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction onClick={async () => {
await db.servers.delete(server.id);
navigate('/');
}}>
Continue
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
)}
</AMFooter>
</main>
</>
);
}
}
export default ServerCardInPage;
@@ -1,22 +1,22 @@
"use client";
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 {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 {LoadingIndicator} from "@/components/loading-indicator";
import {CardCreationDialog} from "@/components/card-creation-form";
import {Button} from "@/components/ui/button";
import {
ContextMenu,
ContextMenuContent,
ContextMenuItem,
ContextMenuTrigger
} from "@/components/ui/context-menu";
import {AMHeader} from "@/components/am-header.tsx";
import {AMFooter} from "@/components/am-footer.tsx";
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,26 +56,26 @@ 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('/')}>
<AMHeader title="Cards">
<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>
</div>
<Separator/>
</AMHeader>
<div className="flex flex-col gap-4 w-full flex-grow">
{cards ? <CardList cards={cards}/> : <LoadingIndicator/>}
</div>
<Footer/>
<AMFooter/>
</main>
);
}
}
export default CardManagerPage;
@@ -1,29 +1,27 @@
"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,
History,
Plus,
ShieldAlert,
SquareArrowOutUpRight,
Trash, WalletCards
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 {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 +29,17 @@ 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";
import {AMFooter} from "@/components/am-footer.tsx";
import {AMHeader} from "@/components/am-header.tsx";
import {AMCard, AMCardDetail} from "@/components/am-card.tsx";
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/>);
@@ -66,31 +61,21 @@ function ServerList(props: { servers: AimeServer[] | undefined }) {
{props.servers.map(s =>
<ContextMenu key={s.id}>
<ContextMenuTrigger>
<Card>
<div className="flex p-6 items-center gap-3">
<div className="flex-grow flex flex-col gap-4">
<h4 className="text-2xl font-semibold">{s.name}</h4>
<div className="text-md flex gap-3 flex-wrap">
{s.lastConnected > 0 && (
<div className="flex items-center gap-2 text-gray-500">
<History className="h-5 w-5"/>
<TimeAgo date={s.lastConnected}/>
</div>
)}
<div className="flex items-center gap-2 text-gray-500">
<Earth className="h-5 w-5"/>
<code>{s.address}</code>
</div>
</div>
</div>
<Button variant="ghost" size="icon" onClick={() => redirectToServerPage(router, s)}>
<ChevronRight className="h-4 w-4"/>
</Button>
</div>
</Card>
<AMCard title={s.name} actionClicked={() => navigate(`/servers/${s.id}`)}>
{s.lastConnected > 0 && (
<AMCardDetail>
<History className="h-5 w-5"/>
<TimeAgo date={s.lastConnected}/>
</AMCardDetail>
)}
<AMCardDetail>
<Earth className="h-5 w-5"/>
<code className="text-ellipsis whitespace-nowrap overflow-hidden">{s.address}</code>
</AMCardDetail>
</AMCard>
</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem inset onClick={() => redirectToServerPage(router, s)}>
<ContextMenuItem inset onClick={() => navigate(`/servers/${s.id}`)}>
Select Server
</ContextMenuItem>
<ContextMenuSeparator/>
@@ -105,7 +90,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 +121,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 +150,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,26 +187,26 @@ 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')}>
<AMHeader title="Servers">
<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>
</div>
<Separator/>
</AMHeader>
<div className="flex flex-col gap-4 w-full flex-grow">
<ServerList servers={servers}/>
</div>
<Footer/>
<AMFooter/>
</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"]
}
+28
View File
@@ -0,0 +1,28 @@
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"),
},
},
build: {
rollupOptions: {
output: {
manualChunks(id: string) {
if(id.includes('radix') || id.includes('lucide') || id.includes('vaul')) {
return 'ui';
}
if (id.includes('react')) {
return 'react';
}
}
}
}
}
})