refactor: remove venueMapBuilder and mrt-map-builder and flatten tools directory

This commit is contained in:
Melvin Chia
2026-01-04 14:53:18 +08:00
parent 8ad85cbcda
commit 271a18c605
279 changed files with 32 additions and 35964 deletions

View File

@@ -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"

View File

@@ -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')}
/>
</>
)

View File

@@ -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 />

View File

@@ -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')
}
})
)

View File

@@ -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

View File

@@ -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 \

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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}'],

View File

@@ -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:*"
}
}
}

View File

@@ -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()
})
}

View File

@@ -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?

View File

@@ -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>

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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');
}

View File

@@ -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>
)

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -1 +0,0 @@
/// <reference types="vite/client" />

View File

@@ -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"
}
]
}

View File

@@ -1,11 +0,0 @@
{
"files": [],
"references": [
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.node.json"
}
]
}

View File

@@ -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"]
}

View File

@@ -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()]
})

View File

@@ -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