mirror of
https://github.com/Lifeforge-app/lifeforge.git
synced 2026-03-03 00:27:01 +00:00
refactor: remove venueMapBuilder and mrt-map-builder and flatten tools directory
This commit is contained in:
@@ -96,7 +96,9 @@ export default function InvoiceCard({
|
||||
<Card
|
||||
isInteractive
|
||||
className="flex-between gap-4"
|
||||
onClick={() => navigate(`/invoice-maker/view/${invoice.id}`)}
|
||||
onClick={() =>
|
||||
navigate(`/melvinchia3636--invoice-maker/view/${invoice.id}`)
|
||||
}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -128,7 +130,9 @@ export default function InvoiceCard({
|
||||
<ContextMenuItem
|
||||
icon="tabler:pencil"
|
||||
label="edit"
|
||||
onClick={() => navigate(`/invoice-maker/modify/${invoice.id}`)}
|
||||
onClick={() =>
|
||||
navigate(`/melvinchia3636--invoice-maker/modify/${invoice.id}`)
|
||||
}
|
||||
/>
|
||||
<ContextMenuItem
|
||||
icon="tabler:files"
|
||||
|
||||
@@ -77,7 +77,7 @@ function InvoiceMaker() {
|
||||
className="hidden md:flex"
|
||||
icon="tabler:plus"
|
||||
namespace="apps.melvinchia3636__invoiceMaker"
|
||||
onClick={() => navigate('/invoice-maker/modify')}
|
||||
onClick={() => navigate('/melvinchia3636--invoice-maker/modify')}
|
||||
>
|
||||
New Invoice
|
||||
</Button>
|
||||
@@ -169,7 +169,7 @@ function InvoiceMaker() {
|
||||
<FAB
|
||||
className="fixed right-6 bottom-6 md:hidden"
|
||||
icon="tabler:plus"
|
||||
onClick={() => navigate('/invoice-maker/modify')}
|
||||
onClick={() => navigate('/melvinchia3636--invoice-maker/modify')}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -17,7 +17,9 @@ function ModifyInvoiceContent() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<GoBackButton onClick={() => navigate('/invoice-maker')} />
|
||||
<GoBackButton
|
||||
onClick={() => navigate('/melvinchia3636--invoice-maker')}
|
||||
/>
|
||||
<Header />
|
||||
<div className="w-full space-y-6 pb-8">
|
||||
<TopInfoSection />
|
||||
|
||||
@@ -120,7 +120,7 @@ function InvoiceEditorProvider({ children }: { children: React.ReactNode }) {
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['invoiceMaker', 'invoices'] })
|
||||
toast.success(t('toast.invoiceCreated'))
|
||||
navigate('/invoice-maker')
|
||||
navigate('/melvinchia3636--invoice-maker')
|
||||
}
|
||||
})
|
||||
)
|
||||
@@ -134,7 +134,7 @@ function InvoiceEditorProvider({ children }: { children: React.ReactNode }) {
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['invoiceMaker', 'invoices'] })
|
||||
toast.success(t('toast.invoiceUpdated'))
|
||||
navigate('/invoice-maker')
|
||||
navigate('/melvinchia3636--invoice-maker')
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
@@ -70,7 +70,7 @@ function Header({
|
||||
<Button
|
||||
as={Link}
|
||||
icon="tabler:pencil"
|
||||
to={`/invoice-maker/modify/${invoice.id}`}
|
||||
to={`/melvinchia3636--invoice-maker/modify/${invoice.id}`}
|
||||
variant="secondary"
|
||||
>
|
||||
Edit
|
||||
|
||||
@@ -22,11 +22,11 @@ COPY server server
|
||||
COPY shared shared
|
||||
COPY packages packages
|
||||
COPY apps apps
|
||||
COPY tools/forgeCLI tools/forgeCLI
|
||||
COPY tools tools
|
||||
COPY tsconfig.json /app/tsconfig.json
|
||||
|
||||
# Create a Docker-specific package.json with only needed workspaces
|
||||
RUN echo '{"workspaces": ["./shared", "./packages/*", "./apps/*", "./server", "./tools/forgeCLI"], "scripts": {"forge": "bun run tools/forgeCLI/src/index.ts"}}' > package.json
|
||||
RUN echo '{"workspaces": ["./shared", "./packages/*", "./apps/*", "./server", "./tools"], "scripts": {"forge": "bun run tools/src/index.ts"}}' > package.json
|
||||
|
||||
# Install all dependencies with cache mount for faster rebuilds
|
||||
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
||||
|
||||
@@ -11,13 +11,13 @@ COPY bun.lock bun.lock
|
||||
COPY server server
|
||||
COPY shared shared
|
||||
COPY packages packages
|
||||
COPY tools/forgeCLI tools/forgeCLI
|
||||
COPY tools tools
|
||||
COPY apps apps
|
||||
COPY locales locales
|
||||
COPY tsconfig.json tsconfig.json
|
||||
|
||||
# Create a Docker-specific package.json with only needed workspaces
|
||||
RUN echo '{"workspaces": ["./shared", "./packages/*", "./apps/*", "./server", "./tools/forgeCLI"]}' > package.json
|
||||
RUN echo '{"workspaces": ["./shared", "./packages/*", "./apps/*", "./server", "./tools"]}' > package.json
|
||||
|
||||
# Install all dependencies for build
|
||||
RUN --mount=type=cache,target=/root/.bun/install/cache \
|
||||
@@ -35,7 +35,7 @@ COPY --from=builder /lifeforge/shared shared
|
||||
COPY --from=builder /lifeforge/packages packages
|
||||
COPY --from=builder /lifeforge/apps apps
|
||||
COPY --from=builder /lifeforge/locales locales
|
||||
COPY --from=builder /lifeforge/tools/forgeCLI tools/forgeCLI
|
||||
COPY --from=builder /lifeforge/tools tools
|
||||
COPY --from=builder /lifeforge/tsconfig.json tsconfig.json
|
||||
COPY --from=builder /lifeforge/package.json package.json
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ For the server side, there are several environment variables that need to be set
|
||||
<section id="the-forge-cli">
|
||||
## The Forge CLI
|
||||
|
||||
The Forge CLI is a command-line interface tool that helps you manage and configure your LifeForge system. It provides a simple way to manage your system and modules. This tool doesn't require any installation, as it's included in the monorepo inside the `tools/forgeCLI` directory. You can run it using bun.
|
||||
The Forge CLI is a command-line interface tool that helps you manage and configure your LifeForge system. It provides a simple way to manage your system and modules. This tool doesn't require any installation, as it's included in the monorepo inside the `tools/` directory. You can run it using bun.
|
||||
|
||||
```bash
|
||||
bun run forge
|
||||
|
||||
@@ -48,7 +48,7 @@ Contains the documentation site for LifeForge, built with React and MDX. This is
|
||||
|
||||
Contains standalone development tools that are part of the LifeForge ecosystem, some of them are publicly available, while some of them requires SSO authentication through the main LifeForge application:
|
||||
|
||||
- **`forgeCLI/`** - Command-line interface tool for managing LifeForge instances, including module management, database migrations, and other administrative tasks. <CustomLink text="Explore ForgeCLI" to="/user-guide/cli/forgecli" />
|
||||
- **`forgeCLI/`** - Command-line interface tool for managing LifeForge instances, including module management, database migrations, and other administrative tasks. <CustomLink text="Explore ForgeCLI" to="/user-guide/cli" />
|
||||
- **`apiBuilder/`** - Interactive tool for building and testing API endpoints
|
||||
- **`apiExplorer/`** - Visual explorer for browsing available API routes
|
||||
- **`localizationManager/`** - Management interface for handling translations and localization
|
||||
|
||||
@@ -93,7 +93,7 @@ import Commit from "../../components/Commit";
|
||||
bun forge db generate-migrations
|
||||
```
|
||||
|
||||
- <Commit hash="b98631f5a5b3b4ec268890908386f5ecb0127ff2" />, <Commit hash="ba3800f382a9efd96d35a09e5616384a1867a4d6" />: ForgeCLI is moved from `scripts/` directory to `tools/forgeCLI/` directory to better reflect its purpose as a development tool for LifeForge ecosystem.
|
||||
- <Commit hash="b98631f5a5b3b4ec268890908386f5ecb0127ff2" />, <Commit hash="ba3800f382a9efd96d35a09e5616384a1867a4d6" />: ForgeCLI is moved from `scripts/` directory to `tools/` directory to better reflect its purpose as a development tool for LifeForge ecosystem.
|
||||
- <Commit hash="35069716aeb13d4429e521feb956e069b90af5b5" />: Moved all code to `src/` directory in the forgeCLI codebase and added `tsconfig.json` for better project structure and TypeScript support.
|
||||
- <Commit hash="eb38fc836d19a568b1f82da8998264b30b06ee75" />: Added colors to console logging in ForgeCLI for better readability.
|
||||
- <Commit hash="f3d16f05247e1867093195a1b91877c4f5e6a5fe" />, <Commit hash="a97697cc22d8afb98f66466412ad31172786d94e" />, <Commit hash="9c44e06bcd34e5318807b1d72c9bff7dc46cff78" />, <Commit hash="be756c8f57e13babcd9038733c25e1c6f06b6316" />: Added database initialization command in ForgeCLI to facilitate setting up the database for new installations. It can be run using:
|
||||
|
||||
@@ -10,12 +10,7 @@ import tseslint from 'typescript-eslint'
|
||||
|
||||
export default [
|
||||
{
|
||||
ignores: [
|
||||
'**/*.config.js',
|
||||
'**/dist/',
|
||||
'dist/',
|
||||
'tools/forgeCLI/src/templates/**'
|
||||
]
|
||||
ignores: ['**/*.config.js', '**/dist/', 'dist/', 'tools/src/templates/**']
|
||||
},
|
||||
{
|
||||
files: ['**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"./tools/*"
|
||||
],
|
||||
"scripts": {
|
||||
"forge": "bun run tools/forgeCLI/src/index.ts"
|
||||
"forge": "bun run tools/src/index.ts"
|
||||
},
|
||||
"keywords": [
|
||||
"bun",
|
||||
@@ -66,4 +66,4 @@
|
||||
"@lifeforge/lifeforge--calendar": "workspace:*",
|
||||
"@lifeforge/melvinchia3636--invoice-maker": "workspace:*"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { Command } from 'commander'
|
||||
|
||||
import { generateModuleRegistries } from '../modules/functions/registry/generator'
|
||||
|
||||
export default function setup(program: Command): void {
|
||||
program
|
||||
.command('generate')
|
||||
.alias('gen')
|
||||
.description('Generate module and locale registries')
|
||||
.action(() => {
|
||||
generateModuleRegistries()
|
||||
})
|
||||
}
|
||||
24
tools/mrt-map-builder/.gitignore
vendored
24
tools/mrt-map-builder/.gitignore
vendored
@@ -1,24 +0,0 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
@@ -1,21 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/svg+xml"
|
||||
href="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBmaWxsPSJjdXJyZW50Q29sb3IiIGQ9Ik00IDE1LjVWNnEwLTEuMzI1LjY4OC0yLjExM3QxLjgxMi0xLjJ0Mi41NjMtLjU1VDEyIDJxMS42NSAwIDMuMTEzLjEzOHQyLjU1LjU1dDEuNzEyIDEuMlQyMCA2djkuNXEwIDEuNDc1LTEuMDEyIDIuNDg4VDE2LjUgMTlsLjUuNXEuNDI1LjQyNS4yLjk2M3QtLjgyNS41MzdxLS4xNzUgMC0uMzM3LS4wNjJ0LS4yODgtLjE4OEwxNCAxOWgtNGwtMS43NSAxLjc1cS0uMTI1LjEyNS0uMjg4LjE4OFQ3LjYyNiAyMXEtLjU3NSAwLS44MTItLjUzN1Q3IDE5LjVsLjUtLjVxLTEuNDc1IDAtMi40ODgtMS4wMTJUNCAxNS41TTEyIDRxLTIuNjUgMC0zLjg3NS4zMTNUNi40NSA1aDExLjJxLS4zNzUtLjQyNS0xLjYxMi0uNzEyVDEyIDRtLTYgNmg1VjdINnptMTAuNSAySDZoMTJ6TTEzIDEwaDVWN2gtNXptLTQuNSA2cS42NSAwIDEuMDc1LS40MjVUMTAgMTQuNXQtLjQyNS0xLjA3NVQ4LjUgMTN0LTEuMDc1LjQyNVQ3IDE0LjV0LjQyNSAxLjA3NVQ4LjUgMTZtNyAwcS42NSAwIDEuMDc1LS40MjVUMTcgMTQuNXQtLjQyNS0xLjA3NVQxNS41IDEzdC0xLjA3NS40MjVUMTQgMTQuNXQuNDI1IDEuMDc1VDE1LjUgMTZtLTggMWg5cS42NSAwIDEuMDc1LS40MjVUMTggMTUuNVYxMkg2djMuNXEwIC42NS40MjUgMS4wNzVUNy41IDE3TTEyIDVoNS42NWgtMTEuMnoiLz48L3N2Zz4="
|
||||
/>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>MRT Map Builder</title>
|
||||
</head>
|
||||
|
||||
<body class="bg-zinc">
|
||||
<div
|
||||
class="dark:bg-bg-950 min-h-dvh w-full bg-white text-black"
|
||||
id="root"
|
||||
></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"name": "mrt-map-builder",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify/react": "^6.0.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"clsx": "^2.1.1",
|
||||
"d3": "^7.9.0",
|
||||
"daisyui": "^5.0.50",
|
||||
"i18next": "^25.6.0",
|
||||
"i18next-http-backend": "2.7.3",
|
||||
"lifeforge-ui": "workspace:*",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.2.0",
|
||||
"react-i18next": "^15.5.1",
|
||||
"shared": "workspace:*",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tinycolor2": "^1.6.0",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.2.0",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@vitejs/plugin-react": "^4.6.0",
|
||||
"globals": "^16.3.0",
|
||||
"vite": "^7.0.4"
|
||||
}
|
||||
}
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,262 +0,0 @@
|
||||
import { Icon } from '@iconify/react'
|
||||
import {
|
||||
Button,
|
||||
ContentWrapperWithSidebar,
|
||||
LayoutWithSidebar,
|
||||
SidebarDivider,
|
||||
SidebarWrapper
|
||||
} from 'lifeforge-ui'
|
||||
import { useRef, useState } from 'react'
|
||||
import { usePersonalization } from 'shared'
|
||||
|
||||
import LineSection from './components/sidebar/LineSection'
|
||||
import SettingsSection from './components/sidebar/SettingsSection'
|
||||
import StationSection from './components/sidebar/StationSection'
|
||||
import useCanvasEvents from './hooks/useCanvasEvents'
|
||||
import useKeyboardEvents from './hooks/useKeyboardEvents'
|
||||
import usePersistence from './hooks/usePersistence'
|
||||
import useRendering from './hooks/useRendering'
|
||||
import type { Line, Settings, Station } from './typescript/mrt.interfaces'
|
||||
|
||||
const D3MRTMap = () => {
|
||||
const { bgTempPalette } = usePersonalization()
|
||||
|
||||
const ref = useRef(null)
|
||||
|
||||
const gRef = useRef(null)
|
||||
|
||||
const [currentlyWorking, _setCurrentWorking] = useState<'station' | 'line'>(
|
||||
'line'
|
||||
)
|
||||
|
||||
const currentWorkingRef = useRef<'station' | 'line'>(currentlyWorking)
|
||||
|
||||
function setCurrentWorking(plotting: 'station' | 'line') {
|
||||
currentWorkingRef.current = plotting
|
||||
_setCurrentWorking(plotting)
|
||||
}
|
||||
|
||||
const [selectedLineIndex, _setSelectedLineIndex] = useState<{
|
||||
index: number | null
|
||||
type: 'path_drawing' | 'station_plotting'
|
||||
}>(
|
||||
localStorage.getItem('mrtSelectedLineIndex')
|
||||
? JSON.parse(
|
||||
localStorage.getItem('mrtSelectedLineIndex') ??
|
||||
'{"index": null, "type": "path_drawing"}'
|
||||
)
|
||||
: null
|
||||
)
|
||||
|
||||
const selectedLineIndexRef = useRef(selectedLineIndex)
|
||||
|
||||
function setSelectedLineIndex(
|
||||
type: 'path_drawing' | 'station_plotting',
|
||||
index: number | null
|
||||
) {
|
||||
selectedLineIndexRef.current = { index, type }
|
||||
_setSelectedLineIndex({ index, type })
|
||||
}
|
||||
|
||||
const [settings, _setSettings] = useState<Settings>(
|
||||
localStorage.getItem('settings')
|
||||
? JSON.parse(
|
||||
localStorage.getItem('settings') ??
|
||||
`{
|
||||
"showImage": false,
|
||||
"bgImagePreview": "",
|
||||
"bgImageScale": 100,
|
||||
"colorOfCurrentLine": "${bgTempPalette[0]}",
|
||||
"darkMode": false
|
||||
}`
|
||||
)
|
||||
: {
|
||||
showImage: false,
|
||||
bgImagePreview: '',
|
||||
bgImageScale: 100,
|
||||
colorOfCurrentLine: bgTempPalette[0],
|
||||
darkMode: false
|
||||
}
|
||||
)
|
||||
|
||||
const setSettings = (newSettings: Partial<typeof settings>) => {
|
||||
_setSettings(prev => ({ ...prev, ...newSettings }))
|
||||
}
|
||||
|
||||
const [mrtStations, setMrtStations] = useState<Station[]>(
|
||||
localStorage.getItem('mrtStations')
|
||||
? JSON.parse(localStorage.getItem('mrtStations') ?? '[]')
|
||||
: []
|
||||
)
|
||||
|
||||
const [mrtLines, setMrtLines] = useState<Line[]>(
|
||||
localStorage.getItem('mrtLines')
|
||||
? JSON.parse(localStorage.getItem('mrtLines') ?? '[]')
|
||||
: []
|
||||
)
|
||||
|
||||
const importData = () => {
|
||||
const input = document.createElement('input')
|
||||
|
||||
input.type = 'file'
|
||||
input.accept = 'application/json'
|
||||
|
||||
input.onchange = async event => {
|
||||
const file = (event.target as HTMLInputElement).files?.[0]
|
||||
|
||||
if (!file) return
|
||||
|
||||
const text = await file.text()
|
||||
|
||||
const data = JSON.parse(text)
|
||||
|
||||
setMrtLines(data.mrtLines)
|
||||
setMrtStations(data.mrtStations)
|
||||
setSettings(data.settings)
|
||||
setSelectedLineIndex(
|
||||
data.selectedLineIndex.type,
|
||||
data.selectedLineIndex.index
|
||||
)
|
||||
}
|
||||
input.click()
|
||||
}
|
||||
|
||||
const exportData = () => {
|
||||
const data = {
|
||||
mrtLines,
|
||||
mrtStations,
|
||||
settings: {
|
||||
...settings,
|
||||
bgImage: null,
|
||||
bgImagePreview: null
|
||||
},
|
||||
selectedLineIndex
|
||||
}
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], {
|
||||
type: 'application/json'
|
||||
})
|
||||
|
||||
const url = URL.createObjectURL(blob)
|
||||
|
||||
const a = document.createElement('a')
|
||||
|
||||
a.href = url
|
||||
a.download = 'mrt-map-data.json'
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
useCanvasEvents({
|
||||
ref,
|
||||
gRef,
|
||||
selectedLineIndexRef,
|
||||
setMrtLines,
|
||||
currentWorkingRef
|
||||
})
|
||||
|
||||
useRendering({
|
||||
gRef,
|
||||
mrtLines,
|
||||
mrtStations,
|
||||
settings,
|
||||
selectedLineIndex,
|
||||
currentlyWorking,
|
||||
setMrtStations
|
||||
})
|
||||
|
||||
useKeyboardEvents({
|
||||
selectedLineIndex,
|
||||
setMrtLines,
|
||||
setMrtStations,
|
||||
currentlyWorking
|
||||
})
|
||||
|
||||
usePersistence({
|
||||
mrtLines,
|
||||
mrtStations,
|
||||
settings,
|
||||
selectedLineIndex
|
||||
})
|
||||
|
||||
return (
|
||||
<main
|
||||
className="bg-bg-200/50 text-bg-800 dark:bg-bg-900/50 dark:text-bg-50 flex h-dvh w-full flex-col p-8 pb-0"
|
||||
id="app"
|
||||
>
|
||||
<LayoutWithSidebar>
|
||||
<SidebarWrapper>
|
||||
<div className="mb-4 flex items-center gap-3 px-4">
|
||||
<Icon className="text-2xl" icon="tabler:mouse" />
|
||||
<h2 className="text-xl font-medium">Currently Working On</h2>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 px-4">
|
||||
<Button
|
||||
className="w-full"
|
||||
icon="tabler:line"
|
||||
variant={currentlyWorking === 'line' ? 'primary' : 'plain'}
|
||||
onClick={() => {
|
||||
setCurrentWorking('line')
|
||||
setSelectedLineIndex('path_drawing', null)
|
||||
}}
|
||||
>
|
||||
Line
|
||||
</Button>
|
||||
<Button
|
||||
className="w-full"
|
||||
icon="tabler:train"
|
||||
variant={currentlyWorking === 'station' ? 'primary' : 'plain'}
|
||||
onClick={() => {
|
||||
setCurrentWorking('station')
|
||||
setSelectedLineIndex('path_drawing', null)
|
||||
}}
|
||||
>
|
||||
Station
|
||||
</Button>
|
||||
</div>
|
||||
<SidebarDivider />
|
||||
{currentlyWorking === 'line' ? (
|
||||
<LineSection
|
||||
mrtLines={mrtLines}
|
||||
selectedLineIndex={selectedLineIndex}
|
||||
setMrtLines={setMrtLines}
|
||||
setSelectedLineIndex={setSelectedLineIndex}
|
||||
/>
|
||||
) : (
|
||||
<StationSection
|
||||
mrtLines={mrtLines}
|
||||
mrtStations={mrtStations}
|
||||
setMrtStations={setMrtStations}
|
||||
/>
|
||||
)}
|
||||
<SidebarDivider />
|
||||
<SettingsSection
|
||||
exportData={exportData}
|
||||
importData={importData}
|
||||
setMrtLines={setMrtLines}
|
||||
setMrtStations={setMrtStations}
|
||||
setSelectedLineIndex={setSelectedLineIndex}
|
||||
setSettings={setSettings}
|
||||
settings={settings}
|
||||
/>
|
||||
</SidebarWrapper>
|
||||
<ContentWrapperWithSidebar>
|
||||
<div className="component-bg mb-8 h-full overflow-hidden rounded-lg">
|
||||
<svg
|
||||
ref={ref}
|
||||
height="100%"
|
||||
style={{ touchAction: 'none' }}
|
||||
width="100%"
|
||||
>
|
||||
<g ref={gRef}></g>
|
||||
</svg>
|
||||
</div>
|
||||
</ContentWrapperWithSidebar>
|
||||
</LayoutWithSidebar>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default D3MRTMap
|
||||
@@ -1,209 +0,0 @@
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
Button,
|
||||
ConfirmationModal,
|
||||
ContextMenu,
|
||||
ContextMenuItem,
|
||||
NumberInput,
|
||||
useModalStore
|
||||
} from 'lifeforge-ui'
|
||||
import { useState } from 'react'
|
||||
import tinycolor from 'tinycolor2'
|
||||
|
||||
import type { Line } from '../typescript/mrt.interfaces'
|
||||
import ModifyLineModal from './ModifyLineModal'
|
||||
|
||||
function LineItem({
|
||||
line,
|
||||
index,
|
||||
setMrtLines,
|
||||
selectedLineIndex,
|
||||
setSelectedLineIndex
|
||||
}: {
|
||||
line: Line
|
||||
index: number
|
||||
setMrtLines: React.Dispatch<React.SetStateAction<Line[]>>
|
||||
selectedLineIndex: {
|
||||
index: number | null
|
||||
type: 'path_drawing' | 'station_plotting'
|
||||
}
|
||||
setSelectedLineIndex: (
|
||||
type: 'path_drawing' | 'station_plotting',
|
||||
index: number | null
|
||||
) => void
|
||||
}) {
|
||||
const open = useModalStore(state => state.open)
|
||||
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
|
||||
return (
|
||||
<div className="border-bg-200 dark:border-bg-800 mx-4 rounded-lg border-2 p-4">
|
||||
<div className="flex-between gap-6">
|
||||
<div className="flex w-full min-w-0 items-center gap-2">
|
||||
{line.code ? (
|
||||
<div
|
||||
className={clsx(
|
||||
'min-h-6 rounded-sm px-2 pb-0.5 font-[LTAIdentityMedium]',
|
||||
tinycolor(line.color).isDark() ? 'text-bg-100' : 'text-bg-800'
|
||||
)}
|
||||
style={{ backgroundColor: line.color }}
|
||||
>
|
||||
{line.code}
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="h-6 w-1 rounded-sm font-[LTAIdentityMedium]"
|
||||
style={{ backgroundColor: line.color }}
|
||||
/>
|
||||
)}
|
||||
<div className="w-full min-w-0 truncate text-lg font-medium">
|
||||
{line.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className={`p-2! ${collapsed ? 'rotate-180' : ''} transition-transform`}
|
||||
icon="tabler:chevron-down"
|
||||
variant="plain"
|
||||
onClick={() => {
|
||||
setCollapsed(!collapsed)
|
||||
}}
|
||||
/>
|
||||
<ContextMenu
|
||||
classNames={{
|
||||
button: 'p-2!'
|
||||
}}
|
||||
>
|
||||
<ContextMenuItem
|
||||
icon="tabler:edit"
|
||||
label="Edit Line"
|
||||
onClick={() => {
|
||||
open(ModifyLineModal, {
|
||||
type: 'update',
|
||||
setLineData: setMrtLines,
|
||||
index,
|
||||
initialData: {
|
||||
name: line.name,
|
||||
color: line.color,
|
||||
code: line.code
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<ContextMenuItem
|
||||
dangerous
|
||||
icon="tabler:trash"
|
||||
label="Delete Line"
|
||||
onClick={() => {
|
||||
open(ConfirmationModal, {
|
||||
title: 'Delete MRT Line',
|
||||
description: 'Are you sure you want to delete this MRT line?',
|
||||
onConfirm: async () => {
|
||||
setMrtLines(prevLines =>
|
||||
prevLines.filter((_, i) => i !== index)
|
||||
)
|
||||
|
||||
if (selectedLineIndex.index === index) {
|
||||
setSelectedLineIndex('path_drawing', null)
|
||||
}
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<ul className="mt-4 flex flex-col gap-2">
|
||||
{line.path.length > 0 ? (
|
||||
line.path.map((point, pointIndex) => (
|
||||
<li
|
||||
key={pointIndex}
|
||||
className="border-bg-200 dark:border-bg-800 space-y-2 rounded-md border-2 p-4"
|
||||
>
|
||||
<NumberInput
|
||||
className="w-full"
|
||||
icon="tabler:square-letter-x"
|
||||
label="X Coordinate"
|
||||
value={point[0]}
|
||||
onChange={value => {
|
||||
setMrtLines(prevLines =>
|
||||
prevLines.map((l, i) => {
|
||||
if (i !== index) return l
|
||||
|
||||
const newPath = [...l.path]
|
||||
|
||||
newPath[pointIndex][0] = value
|
||||
|
||||
return { ...l, path: newPath }
|
||||
})
|
||||
)
|
||||
}}
|
||||
/>
|
||||
<NumberInput
|
||||
className="w-full"
|
||||
icon="tabler:square-letter-y"
|
||||
label="Y Coordinate"
|
||||
value={point[1]}
|
||||
onChange={value => {
|
||||
setMrtLines(prevLines =>
|
||||
prevLines.map((l, i) => {
|
||||
if (i !== index) return l
|
||||
|
||||
const newPath = [...l.path]
|
||||
|
||||
newPath[pointIndex][1] = value
|
||||
|
||||
return { ...l, path: newPath }
|
||||
})
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
))
|
||||
) : (
|
||||
<p className="text-bg-500">
|
||||
No points added yet. Click on the map to add points.
|
||||
</p>
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
<Button
|
||||
className="mt-4 w-full"
|
||||
disabled={
|
||||
selectedLineIndex.index === index &&
|
||||
selectedLineIndex.type === 'path_drawing'
|
||||
}
|
||||
icon="tabler:brush"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setSelectedLineIndex('path_drawing', index)
|
||||
}}
|
||||
>
|
||||
{selectedLineIndex.index === index &&
|
||||
selectedLineIndex.type === 'path_drawing'
|
||||
? 'Currently Drawing'
|
||||
: 'Draw Path'}
|
||||
</Button>
|
||||
<Button
|
||||
className="mt-2 w-full"
|
||||
disabled={
|
||||
selectedLineIndex.index === index &&
|
||||
selectedLineIndex.type === 'station_plotting'
|
||||
}
|
||||
icon="tabler:target-arrow"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
setSelectedLineIndex('station_plotting', index)
|
||||
}}
|
||||
>
|
||||
{selectedLineIndex.index === index &&
|
||||
selectedLineIndex.type === 'station_plotting'
|
||||
? 'Currently Plotting Stations'
|
||||
: 'Plot Stations'}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default LineItem
|
||||
@@ -1,83 +0,0 @@
|
||||
import { FormModal, defineForm } from 'lifeforge-ui'
|
||||
import React from 'react'
|
||||
|
||||
import type { Line } from '../typescript/mrt.interfaces'
|
||||
|
||||
function ModifyLineModal({
|
||||
onClose,
|
||||
data: { type, setLineData, index, initialData }
|
||||
}: {
|
||||
onClose: () => void
|
||||
data: {
|
||||
type: 'create' | 'update'
|
||||
setLineData: React.Dispatch<React.SetStateAction<Line[]>>
|
||||
index?: number
|
||||
initialData?: Partial<Line>
|
||||
}
|
||||
}) {
|
||||
const { formProps } = defineForm<{
|
||||
name: string
|
||||
color: string
|
||||
}>({
|
||||
title: `${type === 'create' ? 'Create' : 'Update'} MRT Line`,
|
||||
icon: type === 'create' ? 'tabler:plus' : 'tabler:pencil',
|
||||
|
||||
onClose,
|
||||
submitButton: 'create'
|
||||
})
|
||||
.typesMap({
|
||||
name: 'text',
|
||||
color: 'color',
|
||||
code: 'text'
|
||||
})
|
||||
.setupFields({
|
||||
name: {
|
||||
label: 'Line Name',
|
||||
placeholder: 'Enter line name',
|
||||
required: true,
|
||||
icon: 'tabler:route'
|
||||
},
|
||||
color: {
|
||||
label: 'Line Color',
|
||||
required: true
|
||||
},
|
||||
code: {
|
||||
label: 'Line Code',
|
||||
placeholder: 'Enter line code',
|
||||
required: true,
|
||||
icon: 'tabler:hash'
|
||||
}
|
||||
})
|
||||
.initialData(initialData)
|
||||
.onSubmit(async data => {
|
||||
setLineData(prev => {
|
||||
if (index !== undefined) {
|
||||
const newData = [...prev]
|
||||
|
||||
newData[index] = {
|
||||
...newData[index],
|
||||
name: data.name,
|
||||
code: data.code,
|
||||
color: data.color
|
||||
}
|
||||
|
||||
return newData
|
||||
}
|
||||
|
||||
return [
|
||||
...prev,
|
||||
{
|
||||
name: data.name,
|
||||
color: data.color,
|
||||
code: data.code,
|
||||
path: []
|
||||
}
|
||||
]
|
||||
})
|
||||
})
|
||||
.build()
|
||||
|
||||
return <FormModal {...formProps} />
|
||||
}
|
||||
|
||||
export default ModifyLineModal
|
||||
@@ -1,412 +0,0 @@
|
||||
import { Icon } from '@iconify/react'
|
||||
import clsx from 'clsx'
|
||||
import {
|
||||
Button,
|
||||
ConfirmationModal,
|
||||
ContextMenu,
|
||||
ContextMenuItem,
|
||||
ListboxInput,
|
||||
ListboxOption,
|
||||
NumberInput,
|
||||
SliderInput,
|
||||
TagsInput,
|
||||
TextInput,
|
||||
useModalStore
|
||||
} from 'lifeforge-ui'
|
||||
import _ from 'lodash'
|
||||
import React, { useState } from 'react'
|
||||
import { usePersonalization } from 'shared'
|
||||
import tinycolor from 'tinycolor2'
|
||||
|
||||
import type { Line, Station } from '../typescript/mrt.interfaces'
|
||||
|
||||
function StationItem({
|
||||
station,
|
||||
mrtLines,
|
||||
setMrtStations
|
||||
}: {
|
||||
station: Station
|
||||
mrtLines: Line[]
|
||||
setMrtStations: React.Dispatch<React.SetStateAction<Station[]>>
|
||||
}) {
|
||||
const open = useModalStore(state => state.open)
|
||||
|
||||
const { bgTempPalette } = usePersonalization()
|
||||
|
||||
const [collapsed, setCollapsed] = useState(true)
|
||||
|
||||
return (
|
||||
<div className="border-bg-200 dark:border-bg-800 rounded-lg border-2 p-4">
|
||||
<div className="flex-between gap-6">
|
||||
<div className="flex w-full min-w-0 items-center gap-2">
|
||||
<Icon
|
||||
className="text-bg-500 size-6 shrink-0"
|
||||
icon={
|
||||
station.type === 'station' ? 'tabler:map-pin' : 'tabler:exchange'
|
||||
}
|
||||
/>
|
||||
<div className="min-w-0 truncate text-lg font-medium">
|
||||
{station.name || 'Unnamed Station'}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{(station.lines || []).length > 0 &&
|
||||
station.lines.sort().map(line => (
|
||||
<span
|
||||
key={line}
|
||||
className="size-2 rounded-full"
|
||||
style={{
|
||||
backgroundColor:
|
||||
mrtLines.find(l => l.name === line)?.color ?? '#333'
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
className={`p-2! ${collapsed ? 'rotate-180' : ''} transition-transform`}
|
||||
icon="tabler:chevron-down"
|
||||
variant="plain"
|
||||
onClick={() => {
|
||||
setCollapsed(!collapsed)
|
||||
}}
|
||||
/>
|
||||
<ContextMenu
|
||||
classNames={{
|
||||
button: 'p-2!'
|
||||
}}
|
||||
>
|
||||
<ContextMenuItem
|
||||
icon="tabler:edit"
|
||||
label="Edit Line"
|
||||
onClick={() => {}}
|
||||
/>
|
||||
<ContextMenuItem
|
||||
dangerous
|
||||
icon="tabler:trash"
|
||||
label="Delete Line"
|
||||
onClick={() => {
|
||||
open(ConfirmationModal, {
|
||||
title: 'Delete Station',
|
||||
description: `Are you sure you want to delete the station "${station.name}"? This action cannot be undone.`,
|
||||
confirmationButton: 'delete',
|
||||
onConfirm: async () => {
|
||||
setMrtStations(prevStations =>
|
||||
prevStations.filter(s => s.id !== station.id)
|
||||
)
|
||||
}
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<div className="mt-4 space-y-3">
|
||||
<TagsInput
|
||||
icon="tabler:code"
|
||||
label="Station Codes"
|
||||
placeholder="Enter station codes..."
|
||||
renderTags={(tag, tagIndex, onRemove) => (
|
||||
<div
|
||||
key={tagIndex}
|
||||
className="flex items-center rounded-full pt-0.5 pr-2 pb-1 pl-3"
|
||||
style={{
|
||||
backgroundColor:
|
||||
mrtLines.find(l => l.code.slice(0, 2) === tag.slice(0, 2))
|
||||
?.color ?? '#ccc',
|
||||
color: tinycolor(
|
||||
mrtLines.find(l => l.code.slice(0, 2) === tag.slice(0, 2))
|
||||
?.color ?? '#ccc'
|
||||
).isDark()
|
||||
? bgTempPalette[100]
|
||||
: bgTempPalette[800]
|
||||
}}
|
||||
>
|
||||
<span className="mr-2 font-[LTAIdentityMedium] text-sm">
|
||||
{tag}
|
||||
</span>
|
||||
<Button
|
||||
className={clsx(
|
||||
'mt-0.5 p-1! hover:bg-transparent',
|
||||
tinycolor(
|
||||
mrtLines.find(l => l.code.slice(0, 2) === tag.slice(0, 2))
|
||||
?.color ?? '#ccc'
|
||||
).isDark()
|
||||
? 'text-bg-100!'
|
||||
: 'text-bg-800!'
|
||||
)}
|
||||
icon="tabler:x"
|
||||
iconClassName="size-4!"
|
||||
variant="plain"
|
||||
onClick={onRemove}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
onChange={codes => {
|
||||
setMrtStations(prevStations =>
|
||||
prevStations.map(s => {
|
||||
if (s.id === station.id) {
|
||||
return { ...s, codes }
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
)
|
||||
}}
|
||||
value={station.codes || []}
|
||||
/>
|
||||
<ListboxInput
|
||||
buttonContent={
|
||||
<div className="flex items-center gap-2">
|
||||
<Icon
|
||||
className="text-lg"
|
||||
icon={
|
||||
station.type === 'station'
|
||||
? 'tabler:map-pin'
|
||||
: 'tabler:exchange'
|
||||
}
|
||||
/>
|
||||
<span className="text-base font-medium">
|
||||
{_.upperFirst(station.type)}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
className="w-full"
|
||||
icon="tabler:category"
|
||||
label="Station Type"
|
||||
onChange={value => {
|
||||
setMrtStations(prevStations =>
|
||||
prevStations.map(s => {
|
||||
if (s.id === station.id) {
|
||||
return { ...s, type: value }
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
)
|
||||
}}
|
||||
value={station.type}
|
||||
>
|
||||
{[
|
||||
{
|
||||
value: 'station',
|
||||
text: 'Station',
|
||||
icon: 'tabler:map-pin'
|
||||
},
|
||||
{
|
||||
value: 'interchange',
|
||||
text: 'Interchange',
|
||||
icon: 'tabler:exchange'
|
||||
}
|
||||
].map(option => (
|
||||
<ListboxOption
|
||||
key={option.value}
|
||||
icon={option.icon}
|
||||
label={option.text}
|
||||
value={option.value}
|
||||
/>
|
||||
))}
|
||||
</ListboxInput>
|
||||
<ListboxInput
|
||||
multiple
|
||||
buttonContent={
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{(station.lines || []).length > 0 ? (
|
||||
station.lines.map(line => (
|
||||
<div key={line} className="flex items-center gap-2">
|
||||
<span
|
||||
key={line}
|
||||
className="size-2 rounded-full"
|
||||
style={{
|
||||
backgroundColor:
|
||||
mrtLines.find(l => l.name === line)?.color ?? '#333'
|
||||
}}
|
||||
/>
|
||||
<span className="max-w-56 truncate text-base font-medium">
|
||||
{line}
|
||||
</span>
|
||||
</div>
|
||||
))
|
||||
) : (
|
||||
<span className="text-bg-500">No lines selected</span>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
icon="tabler:route"
|
||||
label="Lines"
|
||||
onChange={value => {
|
||||
setMrtStations(prevStations =>
|
||||
prevStations.map(s => {
|
||||
if (s.id === station.id) {
|
||||
return { ...s, lines: value }
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
)
|
||||
}}
|
||||
value={station.lines ?? []}
|
||||
>
|
||||
{mrtLines.map(line => (
|
||||
<ListboxOption
|
||||
key={line.name}
|
||||
color={line.color}
|
||||
icon="tabler:route"
|
||||
label={line.name}
|
||||
value={line.name}
|
||||
/>
|
||||
))}
|
||||
</ListboxInput>
|
||||
<TextInput
|
||||
className="flex-1"
|
||||
icon="tabler:map-pin"
|
||||
label="Station Name"
|
||||
placeholder="Station Name"
|
||||
onChange={value => {
|
||||
setMrtStations(prevStations =>
|
||||
prevStations.map(s => {
|
||||
if (s.id === station.id) {
|
||||
return { ...s, name: value }
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
)
|
||||
}}
|
||||
value={station.name}
|
||||
/>
|
||||
<NumberInput
|
||||
className="flex-1"
|
||||
icon="tabler:square-letter-x"
|
||||
label="X Coordinate"
|
||||
onChange={value => {
|
||||
setMrtStations(prevStations =>
|
||||
prevStations.map(s => {
|
||||
if (s.id === station.id) {
|
||||
return { ...s, x: value }
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
)
|
||||
}}
|
||||
value={station.x}
|
||||
/>
|
||||
<NumberInput
|
||||
className="flex-1"
|
||||
icon="tabler:square-letter-y"
|
||||
label="Y Coordinate"
|
||||
onChange={value => {
|
||||
setMrtStations(prevStations =>
|
||||
prevStations.map(s => {
|
||||
if (s.id === station.id) {
|
||||
return { ...s, y: value }
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
)
|
||||
}}
|
||||
value={station.y}
|
||||
/>
|
||||
{station.type === 'interchange' && (
|
||||
<>
|
||||
<NumberInput
|
||||
className="flex-1"
|
||||
icon="tabler:arrows-horizontal"
|
||||
label="Width"
|
||||
onChange={value => {
|
||||
setMrtStations(prevStations =>
|
||||
prevStations.map(s => {
|
||||
if (s.id === station.id) {
|
||||
return { ...s, width: value }
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
)
|
||||
}}
|
||||
value={station.width}
|
||||
/>
|
||||
<NumberInput
|
||||
className="flex-1"
|
||||
icon="tabler:arrows-vertical"
|
||||
label="Height"
|
||||
onChange={value => {
|
||||
setMrtStations(prevStations =>
|
||||
prevStations.map(s => {
|
||||
if (s.id === station.id) {
|
||||
return { ...s, height: value }
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
)
|
||||
}}
|
||||
value={station.height ?? 1}
|
||||
/>
|
||||
<NumberInput
|
||||
className="flex-1"
|
||||
icon="tabler:rotate-2"
|
||||
label="Rotation"
|
||||
onChange={value => {
|
||||
setMrtStations(prevStations =>
|
||||
prevStations.map(s => {
|
||||
if (s.id === station.id) {
|
||||
return { ...s, rotate: value }
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
)
|
||||
}}
|
||||
value={station.rotate ?? 0}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<SliderInput
|
||||
className="flex-1"
|
||||
icon="tabler:arrows-move-horizontal"
|
||||
label="Text Offset X"
|
||||
max={100}
|
||||
min={-100}
|
||||
onChange={value => {
|
||||
setMrtStations(prevStations =>
|
||||
prevStations.map(s => {
|
||||
if (s.id === station.id) {
|
||||
return { ...s, textOffsetX: value }
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
)
|
||||
}}
|
||||
value={station.textOffsetX ?? 0}
|
||||
/>
|
||||
<SliderInput
|
||||
className="flex-1"
|
||||
icon="tabler:arrows-move-vertical"
|
||||
label="Text Offset Y"
|
||||
max={100}
|
||||
min={-100}
|
||||
onChange={value => {
|
||||
setMrtStations(prevStations =>
|
||||
prevStations.map(s => {
|
||||
if (s.id === station.id) {
|
||||
return { ...s, textOffsetY: value }
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
)
|
||||
}}
|
||||
value={station.textOffsetY ?? 0}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default StationItem
|
||||
@@ -1,73 +0,0 @@
|
||||
import { Icon } from '@iconify/react'
|
||||
import { Button, EmptyStateScreen, useModalStore } from 'lifeforge-ui'
|
||||
|
||||
import type { Line } from '../../typescript/mrt.interfaces'
|
||||
import LineItem from '../LineItem'
|
||||
import ModifyLineModal from '../ModifyLineModal'
|
||||
|
||||
function LineSection({
|
||||
mrtLines,
|
||||
setMrtLines,
|
||||
selectedLineIndex,
|
||||
setSelectedLineIndex
|
||||
}: {
|
||||
mrtLines: Line[]
|
||||
setMrtLines: React.Dispatch<React.SetStateAction<Line[]>>
|
||||
selectedLineIndex: {
|
||||
index: number | null
|
||||
type: 'path_drawing' | 'station_plotting'
|
||||
}
|
||||
setSelectedLineIndex: (
|
||||
type: 'path_drawing' | 'station_plotting',
|
||||
index: number | null
|
||||
) => void
|
||||
}) {
|
||||
const open = useModalStore(state => state.open)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex-between mb-4 gap-3 px-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon className="text-2xl" icon="tabler:route" />
|
||||
<h2 className="text-xl font-medium">Lines</h2>
|
||||
</div>
|
||||
<Button
|
||||
className="p-2!"
|
||||
icon="tabler:plus"
|
||||
variant="plain"
|
||||
onClick={() => {
|
||||
open(ModifyLineModal, {
|
||||
type: 'create',
|
||||
setLineData: setMrtLines
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{mrtLines.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{mrtLines.map((line, index) => (
|
||||
<LineItem
|
||||
key={`line-${index}`}
|
||||
index={index}
|
||||
line={line}
|
||||
selectedLineIndex={selectedLineIndex}
|
||||
setMrtLines={setMrtLines}
|
||||
setSelectedLineIndex={setSelectedLineIndex}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyStateScreen
|
||||
smaller
|
||||
icon="tabler:route-off"
|
||||
message={{
|
||||
title: 'No MRT Lines',
|
||||
description: 'Click the button above to add a new MRT line.'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default LineSection
|
||||
@@ -1,160 +0,0 @@
|
||||
import { Icon } from '@iconify/react'
|
||||
import {
|
||||
Button,
|
||||
ColorInput,
|
||||
ConfirmationModal,
|
||||
FileInput,
|
||||
SliderInput,
|
||||
Switch,
|
||||
useModalStore
|
||||
} from 'lifeforge-ui'
|
||||
|
||||
import type { Line, Settings, Station } from '../../typescript/mrt.interfaces'
|
||||
|
||||
function SettingsSection({
|
||||
setMrtLines,
|
||||
setMrtStations,
|
||||
settings,
|
||||
setSettings,
|
||||
setSelectedLineIndex,
|
||||
importData,
|
||||
exportData
|
||||
}: {
|
||||
setMrtLines: React.Dispatch<React.SetStateAction<Line[]>>
|
||||
setMrtStations: React.Dispatch<React.SetStateAction<Station[]>>
|
||||
settings: Settings
|
||||
setSettings: (settings: Partial<Settings>) => void
|
||||
setSelectedLineIndex: (
|
||||
type: 'path_drawing' | 'station_plotting',
|
||||
index: number | null
|
||||
) => void
|
||||
importData: () => void
|
||||
exportData: () => void
|
||||
}) {
|
||||
const open = useModalStore(state => state.open)
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex items-center gap-3 px-4">
|
||||
<Icon className="text-2xl" icon="tabler:settings" />
|
||||
<h2 className="text-xl font-medium">Settings</h2>
|
||||
</div>
|
||||
<div className="space-y-3 px-4">
|
||||
<ColorInput
|
||||
className="w-full"
|
||||
label="Color of Selected Line"
|
||||
value={settings.colorOfCurrentLine}
|
||||
onChange={color => {
|
||||
setSettings({
|
||||
colorOfCurrentLine: color
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<FileInput
|
||||
enableUrl
|
||||
acceptedMimeTypes={{
|
||||
image: ['png', 'jpg', 'jpeg', 'gif', 'webp']
|
||||
}}
|
||||
file={settings.bgImage}
|
||||
icon="tabler:photo"
|
||||
label="Background Image"
|
||||
preview={settings.bgImagePreview}
|
||||
setData={({ file, preview }) => {
|
||||
setSettings({
|
||||
bgImage: file,
|
||||
bgImagePreview: preview
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<SliderInput
|
||||
icon="tabler:zoom-in-area"
|
||||
label="Background Image Scale"
|
||||
max={200}
|
||||
min={10}
|
||||
step={10}
|
||||
value={settings.bgImageScale}
|
||||
wrapperClassName="mb-8"
|
||||
onChange={value => {
|
||||
setSettings({
|
||||
bgImageScale: value
|
||||
})
|
||||
}}
|
||||
/>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-bg-500 flex items-center gap-2">
|
||||
<Icon className="size-6" icon="tabler:eye" />
|
||||
<span className="text-lg">Show background image</span>
|
||||
</div>
|
||||
<Switch
|
||||
value={settings.showImage}
|
||||
onChange={() => {
|
||||
setSettings({
|
||||
showImage: !settings.showImage
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center justify-between">
|
||||
<div className="text-bg-500 flex items-center gap-2">
|
||||
<Icon className="size-6" icon="tabler:moon" />
|
||||
<span className="text-lg">Dark mode</span>
|
||||
</div>
|
||||
<Switch
|
||||
value={settings.darkMode}
|
||||
onChange={() => {
|
||||
setSettings({
|
||||
darkMode: !settings.darkMode
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-6 flex items-center gap-2">
|
||||
<Button
|
||||
className="w-1/2"
|
||||
icon="uil:import"
|
||||
variant="secondary"
|
||||
onClick={importData}
|
||||
>
|
||||
Import
|
||||
</Button>
|
||||
<Button
|
||||
className="w-1/2"
|
||||
icon="uil:export"
|
||||
variant="secondary"
|
||||
onClick={exportData}
|
||||
>
|
||||
Export
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
dangerous
|
||||
className="-mt-2 w-full"
|
||||
icon="tabler:trash"
|
||||
variant="secondary"
|
||||
onClick={() => {
|
||||
open(ConfirmationModal, {
|
||||
title: 'Reset MRT Map',
|
||||
description:
|
||||
'Are you sure you want to reset the MRT map? This action cannot be undone.',
|
||||
onConfirm: async () => {
|
||||
setMrtLines([])
|
||||
setMrtStations([])
|
||||
setSettings({
|
||||
bgImage: null,
|
||||
bgImagePreview: null,
|
||||
showImage: true,
|
||||
darkMode: false
|
||||
})
|
||||
setSelectedLineIndex('path_drawing', null)
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
Reset MRT Map
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default SettingsSection
|
||||
@@ -1,76 +0,0 @@
|
||||
import { Icon } from '@iconify/react'
|
||||
import { EmptyStateScreen, SearchInput } from 'lifeforge-ui'
|
||||
import { useMemo, useState } from 'react'
|
||||
|
||||
import type { Line, Station } from '../../typescript/mrt.interfaces'
|
||||
import StationItem from '../StationItem'
|
||||
|
||||
function StationSection({
|
||||
mrtLines,
|
||||
mrtStations,
|
||||
setMrtStations
|
||||
}: {
|
||||
mrtLines: Line[]
|
||||
mrtStations: Station[]
|
||||
setMrtStations: React.Dispatch<React.SetStateAction<Station[]>>
|
||||
}) {
|
||||
const [searchStationQuery, setSearchStationQuery] = useState('')
|
||||
|
||||
const filteredStations = useMemo(() => {
|
||||
return mrtStations.filter(station =>
|
||||
station.name.toLowerCase().includes(searchStationQuery.toLowerCase())
|
||||
)
|
||||
}, [mrtStations, searchStationQuery])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="mb-4 flex items-center gap-3 px-4">
|
||||
<Icon className="text-2xl" icon="tabler:map-pin" />
|
||||
<h2 className="text-xl font-medium">Stations</h2>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col space-y-3 px-4">
|
||||
<SearchInput
|
||||
className="component-bg-lighter mb-4"
|
||||
searchTarget="stations"
|
||||
value={searchStationQuery}
|
||||
onChange={setSearchStationQuery}
|
||||
/>
|
||||
{mrtStations.length > 0 ? (
|
||||
filteredStations.length > 0 ? (
|
||||
<div className="space-y-3">
|
||||
{filteredStations.map(station => (
|
||||
<StationItem
|
||||
key={station.id}
|
||||
mrtLines={mrtLines}
|
||||
setMrtStations={setMrtStations}
|
||||
station={station}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyStateScreen
|
||||
smaller
|
||||
icon="tabler:search-off"
|
||||
message={{
|
||||
title: 'No Stations Found',
|
||||
description: 'No stations found matching your search query.'
|
||||
}}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
<EmptyStateScreen
|
||||
smaller
|
||||
icon="tabler:map-pin-off"
|
||||
message={{
|
||||
title: 'No MRT Stations',
|
||||
description: 'Click the button below to add a new MRT station.'
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default StationSection
|
||||
@@ -1,68 +0,0 @@
|
||||
import * as d3 from 'd3'
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import type { Line } from '../typescript/mrt.interfaces'
|
||||
|
||||
function useCanvasEvents({
|
||||
ref,
|
||||
gRef,
|
||||
selectedLineIndexRef,
|
||||
setMrtLines,
|
||||
currentWorkingRef
|
||||
}: {
|
||||
ref: React.RefObject<SVGSVGElement | null>
|
||||
gRef: React.RefObject<SVGGElement | null>
|
||||
selectedLineIndexRef: React.RefObject<{
|
||||
index: number | null
|
||||
type: 'path_drawing' | 'station_plotting'
|
||||
}>
|
||||
setMrtLines: React.Dispatch<React.SetStateAction<Line[]>>
|
||||
currentWorkingRef: React.MutableRefObject<'line' | 'station'>
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const svg = d3.select(ref.current)
|
||||
|
||||
const g = d3.select(gRef.current)
|
||||
|
||||
svg.call(
|
||||
d3
|
||||
.zoom()
|
||||
.scaleExtent([0.4, 4])
|
||||
.on('zoom', event => {
|
||||
g.attr('transform', event.transform)
|
||||
}) as never
|
||||
)
|
||||
|
||||
svg.on('click', function (event) {
|
||||
if (event.defaultPrevented) return
|
||||
|
||||
const [x, y] = d3.pointer(event, g.node())
|
||||
|
||||
if (currentWorkingRef.current !== 'line') return
|
||||
|
||||
if (
|
||||
selectedLineIndexRef.current.index === null ||
|
||||
selectedLineIndexRef.current.type !== 'path_drawing'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setMrtLines(prevLines =>
|
||||
prevLines.map((line, index) => {
|
||||
if (
|
||||
index !== selectedLineIndexRef.current.index &&
|
||||
selectedLineIndexRef.current.type !== 'path_drawing'
|
||||
)
|
||||
return line
|
||||
|
||||
return {
|
||||
...line,
|
||||
path: [...line.path, [Math.round(x), Math.round(y)]]
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
}, [])
|
||||
}
|
||||
|
||||
export default useCanvasEvents
|
||||
@@ -1,67 +0,0 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import type { Line, Station } from '../typescript/mrt.interfaces'
|
||||
|
||||
function useKeyboardEvents({
|
||||
selectedLineIndex,
|
||||
setMrtLines,
|
||||
setMrtStations,
|
||||
currentlyWorking
|
||||
}: {
|
||||
selectedLineIndex: {
|
||||
index: number | null
|
||||
type: 'path_drawing' | 'station_plotting'
|
||||
}
|
||||
setMrtLines: React.Dispatch<React.SetStateAction<Line[]>>
|
||||
setMrtStations: React.Dispatch<React.SetStateAction<Station[]>>
|
||||
currentlyWorking: 'line' | 'station'
|
||||
}) {
|
||||
useEffect(() => {
|
||||
document.body.onkeydown = e => {
|
||||
if (e.key === 'z' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
|
||||
if (currentlyWorking === 'line') {
|
||||
if (
|
||||
selectedLineIndex.index === null ||
|
||||
selectedLineIndex.type !== 'path_drawing'
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
setMrtLines(prevLines =>
|
||||
prevLines.map((line, index) => {
|
||||
if (
|
||||
index !== selectedLineIndex.index ||
|
||||
selectedLineIndex.type !== 'path_drawing'
|
||||
)
|
||||
return line
|
||||
|
||||
const newPath = [...line.path]
|
||||
|
||||
newPath.pop()
|
||||
|
||||
return { ...line, path: newPath }
|
||||
})
|
||||
)
|
||||
} else {
|
||||
const confirm = window.confirm(
|
||||
'Are you sure you want to remove the last station? This action cannot be undone.'
|
||||
)
|
||||
|
||||
if (!confirm) return
|
||||
|
||||
setMrtStations(prevStations => {
|
||||
const newStations = [...prevStations]
|
||||
|
||||
newStations.pop()
|
||||
|
||||
return newStations
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [selectedLineIndex, currentlyWorking])
|
||||
}
|
||||
|
||||
export default useKeyboardEvents
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useEffect } from 'react'
|
||||
|
||||
import type { Line, Settings, Station } from '../typescript/mrt.interfaces'
|
||||
|
||||
function usePersistence({
|
||||
mrtLines,
|
||||
mrtStations,
|
||||
settings,
|
||||
selectedLineIndex
|
||||
}: {
|
||||
mrtLines: Line[]
|
||||
mrtStations: Station[]
|
||||
settings: Settings
|
||||
selectedLineIndex: {
|
||||
index: number | null
|
||||
type: 'path_drawing' | 'station_plotting'
|
||||
}
|
||||
}) {
|
||||
useEffect(() => {
|
||||
localStorage.setItem('mrtLines', JSON.stringify(mrtLines))
|
||||
localStorage.setItem('mrtStations', JSON.stringify(mrtStations))
|
||||
localStorage.setItem('settings', JSON.stringify(settings))
|
||||
localStorage.setItem(
|
||||
'mrtSelectedLineIndex',
|
||||
JSON.stringify(selectedLineIndex)
|
||||
)
|
||||
}, [mrtLines, mrtStations, settings, selectedLineIndex])
|
||||
}
|
||||
|
||||
export default usePersistence
|
||||
@@ -1,241 +0,0 @@
|
||||
import * as d3 from 'd3'
|
||||
import { useEffect } from 'react'
|
||||
import { usePersonalization } from 'shared'
|
||||
import { v4 } from 'uuid'
|
||||
|
||||
import type { Line, Settings, Station } from '../typescript/mrt.interfaces'
|
||||
import roundedPolygon from '../utils/roundedPolygon'
|
||||
|
||||
function onStationClicked({
|
||||
mrtLines,
|
||||
mrtStations,
|
||||
selectedLineIndex,
|
||||
setMrtStations,
|
||||
station
|
||||
}: {
|
||||
mrtLines: Line[]
|
||||
mrtStations: Station[]
|
||||
selectedLineIndex: {
|
||||
index: number | null
|
||||
type: 'path_drawing' | 'station_plotting'
|
||||
}
|
||||
setMrtStations: React.Dispatch<React.SetStateAction<Station[]>>
|
||||
station: Station
|
||||
}) {
|
||||
if (
|
||||
selectedLineIndex.type !== 'station_plotting' ||
|
||||
selectedLineIndex.index === null
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const lastStationOfLine =
|
||||
mrtStations
|
||||
.filter(s => s.lines.includes(mrtLines[selectedLineIndex.index!].name))
|
||||
.map(
|
||||
s =>
|
||||
s.codes
|
||||
?.filter(c =>
|
||||
c.startsWith(mrtLines[selectedLineIndex.index!].code.slice(0, 2))
|
||||
)
|
||||
.map(c => parseInt(c.slice(2)) || 0) || []
|
||||
)
|
||||
.flat()
|
||||
.sort((a, b) => a - b)
|
||||
.pop() || 0
|
||||
|
||||
setMrtStations(prevStations =>
|
||||
prevStations.map(s => {
|
||||
if (s.x === station.x && s.y === station.y) {
|
||||
return {
|
||||
...s,
|
||||
codes: [
|
||||
...(s.codes || []),
|
||||
`${mrtLines[selectedLineIndex.index!].code.slice(0, 2)}${lastStationOfLine + 1}`
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function useRendering({
|
||||
gRef,
|
||||
mrtLines,
|
||||
mrtStations,
|
||||
settings,
|
||||
selectedLineIndex,
|
||||
currentlyWorking,
|
||||
setMrtStations
|
||||
}: {
|
||||
gRef: React.RefObject<SVGGElement | null>
|
||||
mrtLines: Line[]
|
||||
mrtStations: Station[]
|
||||
settings: Settings
|
||||
selectedLineIndex: {
|
||||
index: number | null
|
||||
type: 'path_drawing' | 'station_plotting'
|
||||
}
|
||||
currentlyWorking: 'line' | 'station'
|
||||
setMrtStations: React.Dispatch<React.SetStateAction<Station[]>>
|
||||
}) {
|
||||
const { bgTempPalette } = usePersonalization()
|
||||
|
||||
useEffect(() => {
|
||||
const g = d3.select(gRef.current)
|
||||
|
||||
g.selectAll('*').remove()
|
||||
|
||||
if (settings.showImage && settings.bgImagePreview) {
|
||||
g.append('svg:image')
|
||||
.attr('xlink:href', settings.bgImagePreview)
|
||||
.attr('width', `${settings.bgImageScale}%`)
|
||||
.attr('preserveAspectRatio', 'xMidYMid meet')
|
||||
}
|
||||
|
||||
for (let index = 0; index < mrtLines.length; index++) {
|
||||
const line = mrtLines[index]
|
||||
|
||||
if (line.path.length === 0) continue
|
||||
|
||||
const path = roundedPolygon(
|
||||
line.path.map(p => ({ x: p[0], y: p[1] })),
|
||||
5
|
||||
)
|
||||
|
||||
g.append('path')
|
||||
.attr('d', path)
|
||||
.attr('fill', 'none')
|
||||
.attr(
|
||||
'stroke',
|
||||
selectedLineIndex.index === index &&
|
||||
selectedLineIndex.type === 'path_drawing'
|
||||
? settings.colorOfCurrentLine
|
||||
: line.color
|
||||
)
|
||||
.attr('stroke-width', 5)
|
||||
.attr('stroke-linecap', 'round')
|
||||
.on('click', event => {
|
||||
event.stopPropagation()
|
||||
|
||||
if (currentlyWorking !== 'station') return
|
||||
|
||||
const [x, y] = d3.pointer(event, g.node())
|
||||
|
||||
setMrtStations(prevStations => [
|
||||
...prevStations,
|
||||
{
|
||||
id: v4(),
|
||||
x: Math.round(x),
|
||||
y: Math.round(y),
|
||||
name: '',
|
||||
type: 'station' as const,
|
||||
lines: [line.name],
|
||||
rotate: 0,
|
||||
width: 1,
|
||||
height: 1,
|
||||
textOffsetX: 0,
|
||||
textOffsetY: 0
|
||||
}
|
||||
])
|
||||
})
|
||||
}
|
||||
|
||||
for (const station of mrtStations) {
|
||||
if (station.type === 'interchange') {
|
||||
g.append('rect')
|
||||
.attr('x', station.x - 10)
|
||||
.attr('y', station.y - 10)
|
||||
.attr('width', 20 * (station.width || 1))
|
||||
.attr('height', 20 * (station.height || 1))
|
||||
.attr('fill', bgTempPalette[settings.darkMode ? 900 : 100])
|
||||
.attr('stroke', bgTempPalette[settings.darkMode ? 100 : 800])
|
||||
.attr('stroke-width', 3)
|
||||
.attr('rx', 10)
|
||||
.attr('ry', 10)
|
||||
.attr(
|
||||
'transform',
|
||||
`rotate(${station.rotate}, ${station.x}, ${station.y})`
|
||||
)
|
||||
.on('click', () => {
|
||||
onStationClicked({
|
||||
mrtLines,
|
||||
mrtStations,
|
||||
selectedLineIndex,
|
||||
setMrtStations,
|
||||
station
|
||||
})
|
||||
})
|
||||
} else if (station.type === 'station') {
|
||||
g.append('circle')
|
||||
.attr('cx', station.x)
|
||||
.attr('cy', station.y)
|
||||
.attr('r', 6)
|
||||
.attr('fill', bgTempPalette[settings.darkMode ? 900 : 100])
|
||||
.attr(
|
||||
'stroke',
|
||||
mrtLines.find(l => l.name === station.lines?.[0])?.color ||
|
||||
bgTempPalette[settings.darkMode ? 100 : 800]
|
||||
)
|
||||
.attr('stroke-width', 2)
|
||||
.on('click', () => {
|
||||
onStationClicked({
|
||||
mrtLines,
|
||||
mrtStations,
|
||||
selectedLineIndex,
|
||||
setMrtStations,
|
||||
station
|
||||
})
|
||||
})
|
||||
}
|
||||
g.append('text')
|
||||
.attr(
|
||||
'x',
|
||||
station.x +
|
||||
(station.type === 'station' ? 0 : -10 + 10 * (station.width || 1))
|
||||
)
|
||||
.attr(
|
||||
'y',
|
||||
station.y +
|
||||
(station.type === 'station' ? 2 : -8 + 10 * (station.height || 1))
|
||||
)
|
||||
.attr('text-anchor', 'middle')
|
||||
.text(station.codes?.join(', ') || '')
|
||||
.attr('font-size', 5)
|
||||
.attr(
|
||||
'transform',
|
||||
`rotate(${station.rotate}, ${station.x}, ${station.y})`
|
||||
)
|
||||
.attr('fill', bgTempPalette[settings.darkMode ? 100 : 800])
|
||||
.attr('font-family', 'LTAIdentityMedium')
|
||||
|
||||
g.append('text')
|
||||
.attr('x', station.x + (station.textOffsetX || 0))
|
||||
.attr('y', station.y + (station.textOffsetY || 0))
|
||||
.attr('text-anchor', 'middle')
|
||||
.attr('fill', bgTempPalette[settings.darkMode ? 100 : 800])
|
||||
.attr('dominant-baseline', 'middle')
|
||||
.attr('font-size', 10)
|
||||
.attr('font-family', 'LTAIdentityMedium')
|
||||
.attr('white-space', 'pre')
|
||||
.attr('style', 'line-height: 1.2')
|
||||
.each(function () {
|
||||
const textElement = d3.select(this)
|
||||
|
||||
const lines = station.name.split('\\n')
|
||||
|
||||
lines.forEach((line: string, i: number) => {
|
||||
textElement
|
||||
.append('tspan')
|
||||
.attr('x', station.x + (station.textOffsetX || 0))
|
||||
.attr('dy', i === 0 ? '0em' : '1.2em')
|
||||
.text(line)
|
||||
})
|
||||
})
|
||||
}
|
||||
}, [mrtLines, mrtStations, settings, selectedLineIndex, currentlyWorking])
|
||||
}
|
||||
|
||||
export default useRendering
|
||||
@@ -1,72 +0,0 @@
|
||||
import i18n from 'i18next'
|
||||
import I18NextHttpBackend from 'i18next-http-backend'
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
|
||||
import forgeAPI from './utils/forgeAPI'
|
||||
|
||||
i18n
|
||||
.use(I18NextHttpBackend)
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
lng: 'en',
|
||||
cache: {
|
||||
enabled: true
|
||||
},
|
||||
initImmediate: true,
|
||||
maxRetries: 1,
|
||||
react: {
|
||||
useSuspense: true,
|
||||
bindI18n: 'languageChanged loaded'
|
||||
},
|
||||
cleanCode: true,
|
||||
debug: false,
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
},
|
||||
returnedObjectHandler: (key, value, options) => {
|
||||
return JSON.stringify({ key, value, options })
|
||||
},
|
||||
fallbackNS: false,
|
||||
defaultNS: false,
|
||||
saveMissing: true,
|
||||
missingKeyHandler: async (_, namespace, key) => {
|
||||
if (!['apps', 'common'].includes(namespace?.split('.')[0])) {
|
||||
return
|
||||
}
|
||||
|
||||
await forgeAPI.locales.notifyMissing.mutate({
|
||||
namespace,
|
||||
key
|
||||
})
|
||||
},
|
||||
backend: {
|
||||
loadPath: (langs: string[], namespaces: string[]) => {
|
||||
if (
|
||||
!['en', 'zh-TW', 'zh-CN', 'ms'].includes(langs[0]) ||
|
||||
!namespaces.filter(e => e && e !== 'undefined').length
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const [namespace, subnamespace] = namespaces[0].split('.')
|
||||
|
||||
if (!['apps', 'common'].includes(namespace)) {
|
||||
return
|
||||
}
|
||||
|
||||
return forgeAPI.locales.getLocale.input({
|
||||
lang: langs[0] as 'en' | 'zh' | 'zh-TW' | 'zh-CN' | 'ms',
|
||||
namespace: namespace as 'apps' | 'common',
|
||||
subnamespace: subnamespace
|
||||
}).endpoint
|
||||
},
|
||||
parse: (data: string) => {
|
||||
return JSON.parse(data).data
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
console.error('Failed to initialize i18n')
|
||||
})
|
||||
|
||||
export default i18n
|
||||
@@ -1,16 +0,0 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,100..800;1,100..800&family=Onest:wght@100..900&display=swap')
|
||||
layer(base);
|
||||
|
||||
@import 'tailwindcss';
|
||||
|
||||
@plugin 'daisyui';
|
||||
|
||||
@import 'lifeforge-ui/dist/index.css';
|
||||
@source '../../../packages/lifeforge-ui/dist';
|
||||
|
||||
@font-face {
|
||||
font-family: 'LTAIdentityMedium';
|
||||
src:
|
||||
url('/fonts/LTAIdentity-Medium.woff2') format('woff2'),
|
||||
url('/fonts/LTAIdentity-Medium.woff') format('woff');
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { ModalManager } from 'lifeforge-ui'
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import { I18nextProvider } from 'react-i18next'
|
||||
import {
|
||||
APIEndpointProvider,
|
||||
PersonalizationProvider,
|
||||
ToastProvider
|
||||
} from 'shared'
|
||||
|
||||
import App from './App'
|
||||
import i18n from './i18n'
|
||||
import './index.css'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<I18nextProvider i18n={i18n}>
|
||||
<APIEndpointProvider endpoint={import.meta.env.VITE_API_ENDPOINT}>
|
||||
<PersonalizationProvider
|
||||
defaultValueOverride={{
|
||||
rawThemeColor: 'theme-lime',
|
||||
language: 'en'
|
||||
}}
|
||||
>
|
||||
<ToastProvider>
|
||||
<App />
|
||||
<ModalManager />
|
||||
</ToastProvider>
|
||||
</PersonalizationProvider>
|
||||
</APIEndpointProvider>
|
||||
</I18nextProvider>
|
||||
</StrictMode>
|
||||
)
|
||||
@@ -1,30 +0,0 @@
|
||||
export interface Station {
|
||||
id: string
|
||||
x: number
|
||||
y: number
|
||||
name: string
|
||||
lines: string[]
|
||||
type: 'station' | 'interchange'
|
||||
width: number
|
||||
height: number
|
||||
rotate?: number
|
||||
codes?: string[]
|
||||
textOffsetX?: number
|
||||
textOffsetY?: number
|
||||
}
|
||||
|
||||
export interface Line {
|
||||
color: string
|
||||
name: string
|
||||
code: string
|
||||
path: [number, number][]
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
bgImage: string | File | null
|
||||
bgImageScale: number
|
||||
bgImagePreview: string | null
|
||||
showImage: boolean
|
||||
darkMode: boolean
|
||||
colorOfCurrentLine: string
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { createForgeAPIClient } from 'shared'
|
||||
|
||||
import { type AppRoutes } from '../../../../server/src/core/routes/routes.type'
|
||||
|
||||
const forgeAPI = createForgeAPIClient<AppRoutes>(
|
||||
import.meta.env.VITE_API_HOST || 'https://localhost:3000'
|
||||
)
|
||||
|
||||
export default forgeAPI
|
||||
@@ -1,34 +0,0 @@
|
||||
const roundedPolygon = (points: { x: number; y: number }[], radius: number) => {
|
||||
const qb = []
|
||||
|
||||
for (let index = 0; index < points.length; index++) {
|
||||
const first = points[index]
|
||||
|
||||
const second = points[(index + 1) % points.length]
|
||||
|
||||
const distance = Math.hypot(first.x - second.x, first.y - second.y)
|
||||
|
||||
const ratio = radius / distance
|
||||
|
||||
const dx = (second.x - first.x) * ratio
|
||||
|
||||
const dy = (second.y - first.y) * ratio
|
||||
|
||||
qb.push({ x: first.x + dx, y: first.y + dy })
|
||||
qb.push({ x: second.x - dx, y: second.y - dy })
|
||||
}
|
||||
|
||||
let path = `M ${qb[0].x}, ${qb[0].y} L ${qb[1].x}, ${qb[1].y}`
|
||||
|
||||
for (let index = 1; index < points.length; index++) {
|
||||
path += ` Q ${points[index].x},${points[index].y} ${qb[index * 2].x}, ${
|
||||
qb[index * 2].y
|
||||
}`
|
||||
path += ` L ${qb[index * 2 + 1].x}, ${qb[index * 2 + 1].y}`
|
||||
}
|
||||
path += ` Q ${points[0].x},${points[0].y} ${qb[0].x}, ${qb[0].y} Z`
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
export default roundedPolygon
|
||||
1
tools/mrt-map-builder/src/vite-env.d.ts
vendored
1
tools/mrt-map-builder/src/vite-env.d.ts
vendored
@@ -1 +0,0 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,31 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"],
|
||||
"typeRoots": ["../../server/src"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../../server/tsconfig.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'node:path'
|
||||
import { defineConfig } from 'vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
envDir: path.resolve(__dirname, './env'),
|
||||
plugins: [react(), tailwindcss()]
|
||||
})
|
||||
@@ -35,7 +35,7 @@ export async function promptModuleType(): Promise<
|
||||
export function checkModuleTypeAvailability(
|
||||
moduleType: keyof typeof AVAILABLE_TEMPLATE_MODULE_TYPES
|
||||
): void {
|
||||
const templateDir = `${process.cwd()}/tools/forgeCLI/src/templates/${moduleType}`
|
||||
const templateDir = `${process.cwd()}/tools/src/templates/${moduleType}`
|
||||
|
||||
if (!fs.existsSync(templateDir)) {
|
||||
CLILoggingService.error(
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user