Merge remote-tracking branch 'origin/master'
@@ -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?
|
||||
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": "next/core-web-vitals"
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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'
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { MetadataRoute } from 'next'
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
return {
|
||||
rules: {
|
||||
userAgent: '*',
|
||||
disallow: '/'
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
export function Footer() {
|
||||
return (<footer>
|
||||
<span className="text-gray-500 text-sm">AimeNet © 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>
|
||||
}
|
||||
@@ -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},
|
||||
],
|
||||
},
|
||||
})
|
||||
@@ -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>
|
||||
@@ -1,4 +0,0 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -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 |
@@ -0,0 +1,2 @@
|
||||
User-Agent: *
|
||||
Disallow: /
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 © ppc {new Date().getUTCFullYear()}</span>
|
||||
{props.children}
|
||||
</footer>);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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't support HTTPS out-the-box.
|
||||
If you'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'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;
|
||||
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -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")],
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||