GarageApp and wisher projects added
GarageApp set up with translation with i18n, this required changes to routes in src/index.tsx
This commit is contained in:
parent
5469a93a57
commit
fc84d8c479
17 changed files with 529 additions and 1 deletions
36
bun.lock
36
bun.lock
|
|
@ -4,8 +4,14 @@
|
||||||
"": {
|
"": {
|
||||||
"name": "bun-react-template",
|
"name": "bun-react-template",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"i18next": "^25.3.2",
|
||||||
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
|
"i18next-fs-backend": "^2.6.0",
|
||||||
|
"i18next-http-backend": "^3.0.2",
|
||||||
|
"node": "^22.18.0",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19",
|
"react-dom": "^19",
|
||||||
|
"react-i18next": "^15.6.1",
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/bun": "latest",
|
"@types/bun": "latest",
|
||||||
|
|
@ -15,6 +21,8 @@
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"packages": {
|
"packages": {
|
||||||
|
"@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="],
|
||||||
|
|
||||||
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
|
"@types/bun": ["@types/bun@1.2.19", "", { "dependencies": { "bun-types": "1.2.19" } }, "sha512-d9ZCmrH3CJ2uYKXQIUuZ/pUnTqIvLDS0SK7pFmbx8ma+ziH/FRMoAq5bYpRG7y+w1gl+HgyNZbtqgMq4W4e2Lg=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
|
"@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="],
|
||||||
|
|
@ -25,14 +33,42 @@
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
|
"bun-types": ["bun-types@1.2.19", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-uAOTaZSPuYsWIXRpj7o56Let0g/wjihKCkeRqUBhlLVM/Bt+Fj9xTo+LhC1OV1XDaGkz4hNC80et5xgy+9KTHQ=="],
|
||||||
|
|
||||||
|
"cross-fetch": ["cross-fetch@4.0.0", "", { "dependencies": { "node-fetch": "^2.6.12" } }, "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g=="],
|
||||||
|
|
||||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||||
|
|
||||||
|
"html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
|
||||||
|
|
||||||
|
"i18next": ["i18next@25.3.2", "", { "dependencies": { "@babel/runtime": "^7.27.6" }, "peerDependencies": { "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-JSnbZDxRVbphc5jiptxr3o2zocy5dEqpVm9qCGdJwRNO+9saUJS0/u4LnM/13C23fUEWxAylPqKU/NpMV/IjqA=="],
|
||||||
|
|
||||||
|
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.0", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-P+3zEKLnOF0qmiesW383vsLdtQVyKtCNA9cjSoKCppTKPQVfKd2W8hbVo5ZhNJKDqeM7BOcvNoKJOjpHh4Js9g=="],
|
||||||
|
|
||||||
|
"i18next-fs-backend": ["i18next-fs-backend@2.6.0", "", {}, "sha512-3ZlhNoF9yxnM8pa8bWp5120/Ob6t4lVl1l/tbLmkml/ei3ud8IWySCHt2lrY5xWRlSU5D9IV2sm5bEbGuTqwTw=="],
|
||||||
|
|
||||||
|
"i18next-http-backend": ["i18next-http-backend@3.0.2", "", { "dependencies": { "cross-fetch": "4.0.0" } }, "sha512-PdlvPnvIp4E1sYi46Ik4tBYh/v/NbYfFFgTjkwFl0is8A18s7/bx9aXqsrOax9WUbeNS6mD2oix7Z0yGGf6m5g=="],
|
||||||
|
|
||||||
|
"node": ["node@22.18.0", "", { "dependencies": { "node-bin-setup": "^1.0.0" }, "bin": { "node": "bin/node" } }, "sha512-3njku3qUgOVps16gg1+y1L9AseQoZFUZmd2ASA3+K/pryQLoiEoHkjQ6UmVSysW2EkvdILBKsUYM9RpBKJ47+w=="],
|
||||||
|
|
||||||
|
"node-bin-setup": ["node-bin-setup@1.1.4", "", {}, "sha512-vWNHOne0ZUavArqPP5LJta50+S8R261Fr5SvGul37HbEDcowvLjwdvd0ZeSr0r2lTSrPxl6okq9QUw8BFGiAxA=="],
|
||||||
|
|
||||||
|
"node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="],
|
||||||
|
|
||||||
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
|
||||||
|
|
||||||
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
|
||||||
|
|
||||||
|
"react-i18next": ["react-i18next@15.6.1", "", { "dependencies": { "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { "i18next": ">= 23.2.3", "react": ">= 16.8.0", "typescript": "^5" }, "optionalPeers": ["typescript"] }, "sha512-uGrzSsOUUe2sDBG/+FJq2J1MM+Y4368/QW8OLEKSFvnDflHBbZhSd1u3UkW0Z06rMhZmnB/AQrhCpYfE5/5XNg=="],
|
||||||
|
|
||||||
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
|
||||||
|
|
||||||
|
"tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
"undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
|
||||||
|
|
||||||
|
"void-elements": ["void-elements@3.1.0", "", {}, "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w=="],
|
||||||
|
|
||||||
|
"webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="],
|
||||||
|
|
||||||
|
"whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,14 @@
|
||||||
"start": "NODE_ENV=production bun src/index.tsx"
|
"start": "NODE_ENV=production bun src/index.tsx"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"i18next": "^25.3.2",
|
||||||
|
"i18next-browser-languagedetector": "^8.2.0",
|
||||||
|
"i18next-fs-backend": "^2.6.0",
|
||||||
|
"i18next-http-backend": "^3.0.2",
|
||||||
|
"node": "^22.18.0",
|
||||||
"react": "^19",
|
"react": "^19",
|
||||||
"react-dom": "^19"
|
"react-dom": "^19",
|
||||||
|
"react-i18next": "^15.6.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { APITester } from "./APITester";
|
import { APITester } from "./APITester";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
import { List } from "./wisher/list";
|
||||||
|
|
||||||
import logo from "./logo.svg";
|
import logo from "./logo.svg";
|
||||||
import reactLogo from "./react.svg";
|
import reactLogo from "./react.svg";
|
||||||
|
|
@ -17,6 +18,7 @@ export function App() {
|
||||||
Edit <code>src/App.tsx</code> and save to test HMR
|
Edit <code>src/App.tsx</code> and save to test HMR
|
||||||
</p>
|
</p>
|
||||||
<APITester />
|
<APITester />
|
||||||
|
<List />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
28
src/GarageApp/App.tsx
Normal file
28
src/GarageApp/App.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import { APITester } from "../APITester";
|
||||||
|
import "../index.css";
|
||||||
|
import { List } from "../wisher/list";
|
||||||
|
import { useTranslation, withTranslation, Trans } from 'react-i18next';
|
||||||
|
|
||||||
|
import logo from "./logo.svg";
|
||||||
|
import reactLogo from "./react.svg";
|
||||||
|
|
||||||
|
export function App() {
|
||||||
|
const { t, i18n } = useTranslation();
|
||||||
|
return (
|
||||||
|
<div className="app">
|
||||||
|
<div className="logo-container">
|
||||||
|
<img src={logo} alt="Bun Logo" className="logo bun-logo" />
|
||||||
|
<img src={reactLogo} alt="React Logo" className="logo react-logo" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h1>Bun + React | GarageApp</h1>
|
||||||
|
<p>
|
||||||
|
Edit <code>src/App.tsx</code> and save to test HMR
|
||||||
|
</p>
|
||||||
|
<h1>{t ('statistics')}</h1>
|
||||||
|
<h1>{t ('yourvehicles')}</h1>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
27
src/GarageApp/frontend.tsx
Normal file
27
src/GarageApp/frontend.tsx
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
/**
|
||||||
|
* This file is the entry point for the React app, it sets up the root
|
||||||
|
* element and renders the App component to the DOM.
|
||||||
|
*
|
||||||
|
* It is included in `src/index.html`.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { StrictMode } from "react";
|
||||||
|
import { createRoot } from "react-dom/client";
|
||||||
|
import { App } from "./App.tsx";
|
||||||
|
import './i18n';
|
||||||
|
|
||||||
|
const elem = document.getElementById("root")!;
|
||||||
|
const app = (
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (import.meta.hot) {
|
||||||
|
// With hot module reloading, `import.meta.hot.data` is persisted.
|
||||||
|
const root = (import.meta.hot.data.root ??= createRoot(elem));
|
||||||
|
root.render(app);
|
||||||
|
} else {
|
||||||
|
// The hot module reloading API is not available in production.
|
||||||
|
createRoot(elem).render(app);
|
||||||
|
}
|
||||||
22
src/GarageApp/i18n.js
Normal file
22
src/GarageApp/i18n.js
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import i18n from 'i18next';
|
||||||
|
import Backend from 'i18next-http-backend';
|
||||||
|
// import Backend from 'i18next-fs-backend'
|
||||||
|
import LanguageDetector from 'i18next-browser-languagedetector';
|
||||||
|
import { initReactI18next } from 'react-i18next';
|
||||||
|
|
||||||
|
|
||||||
|
i18n
|
||||||
|
.use(Backend)
|
||||||
|
.use(LanguageDetector)
|
||||||
|
.use(initReactI18next)
|
||||||
|
.init({
|
||||||
|
fallbackLng: 'en',
|
||||||
|
debug: true,
|
||||||
|
backend: {
|
||||||
|
loadPath: '/GarageApp/locales/{{lng}}/{{ns}}.json'
|
||||||
|
},
|
||||||
|
lng: 'en',
|
||||||
|
ns: ['translation']
|
||||||
|
|
||||||
|
});
|
||||||
|
export default i18n
|
||||||
13
src/GarageApp/index.html
Normal file
13
src/GarageApp/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="./logo.svg" />
|
||||||
|
<title>Bun + React</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./frontend.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
src/GarageApp/locales/en/.translation.json.swp
Normal file
BIN
src/GarageApp/locales/en/.translation.json.swp
Normal file
Binary file not shown.
231
src/GarageApp/locales/en/translation.json
Normal file
231
src/GarageApp/locales/en/translation.json
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
{
|
||||||
|
"quickentry": "No Quick Entries | Quick Entry | Quick Entries",
|
||||||
|
"statistics": "Statistics",
|
||||||
|
"thisweek": "This week",
|
||||||
|
"thismonth": "This month",
|
||||||
|
"pastxdays": "Past one day | Past {count} days",
|
||||||
|
"pastxmonths": "Past one month | Past {count} months",
|
||||||
|
"thisyear": "This year",
|
||||||
|
"alltime": "All Time",
|
||||||
|
"noattachments": "No Attachments so far",
|
||||||
|
"attachments": "Attachments",
|
||||||
|
"choosefile": "Choose File",
|
||||||
|
"addattachment": "Add Attachment",
|
||||||
|
"sharedwith": "Shared with",
|
||||||
|
"share": "Share",
|
||||||
|
"you": "You",
|
||||||
|
"addfillup": "Add Fillup",
|
||||||
|
"createfillup": "Create Fillup",
|
||||||
|
"deletefillup": "Delete this fillup",
|
||||||
|
"addexpense": "Add Expense",
|
||||||
|
"createexpense": "Create Expense",
|
||||||
|
"deleteexpense": "Delete this expense",
|
||||||
|
"nofillups": "No Fillups so far",
|
||||||
|
"transfervehicle": "Transfer Vehicle",
|
||||||
|
"settingssaved": "Settings saved successfully",
|
||||||
|
"yoursettings": "Your Settings",
|
||||||
|
"settings": "Settings",
|
||||||
|
"changepassword": "Change password",
|
||||||
|
"oldpassword": "Old password",
|
||||||
|
"newpassword": "New password",
|
||||||
|
"repeatnewpassword": "Repeat New Password",
|
||||||
|
"passworddontmatch": "Password values don't match",
|
||||||
|
"save": "Save",
|
||||||
|
"supportthedeveloper": "Support the developer",
|
||||||
|
"buyhimabeer": "Buy him a beer!",
|
||||||
|
"featurerequest": "Feature Request",
|
||||||
|
"foundabug": "Found a bug",
|
||||||
|
"currentversion": "Current Version",
|
||||||
|
"moreinfo": "More Info",
|
||||||
|
"currency": "Currency",
|
||||||
|
"distanceunit": "Distance Unit",
|
||||||
|
"dateformat": "Date Format",
|
||||||
|
"createnow": "Create Now",
|
||||||
|
"yourvehicles": "Your Vehicles",
|
||||||
|
"menu": {
|
||||||
|
"quickentries": "Quick Entries",
|
||||||
|
"logout": "Log out",
|
||||||
|
"import": "Import",
|
||||||
|
"home": "Home",
|
||||||
|
"settings": "Settings",
|
||||||
|
"admin": "Admin",
|
||||||
|
"sitesettings": "Site Settings",
|
||||||
|
"users": "Users",
|
||||||
|
"login": "Log in"
|
||||||
|
},
|
||||||
|
"enterusername": "Enter your username",
|
||||||
|
"enterpassword": "Enter your password",
|
||||||
|
"email": "Email",
|
||||||
|
"password": "Password",
|
||||||
|
"login": "log in",
|
||||||
|
"totalexpenses": "Total Expenses",
|
||||||
|
"fillupcost": "Fillup Costs",
|
||||||
|
"otherexpenses": "Other Expenses",
|
||||||
|
"addvehicle": "Add Vehicle",
|
||||||
|
"editvehicle": "Edit Vehicle",
|
||||||
|
"deletevehicle": "Delete Vehicle",
|
||||||
|
"sharevehicle": "Share vehicle",
|
||||||
|
"makeowner": "Make Owner",
|
||||||
|
"lastfillup": "Last Fillup",
|
||||||
|
"quickentrydesc": "Take a pic of the invoice or the fuel pump display to make an entry later.",
|
||||||
|
"quickentrycreatedsuccessfully": "Quick Entry Created Successfully",
|
||||||
|
"uploadfile": "Upload File",
|
||||||
|
"uploadphoto": "Upload Photo",
|
||||||
|
"details": "Details",
|
||||||
|
"odometer": "Odometer",
|
||||||
|
"language": "Language",
|
||||||
|
"date": "Date",
|
||||||
|
"pastfillups": "Past Fillups",
|
||||||
|
"fuelsubtype": "Fuel Subtype",
|
||||||
|
"fueltype": "Fuel Type",
|
||||||
|
"quantity": "Quantity",
|
||||||
|
"gasstation": "Gas Station",
|
||||||
|
"fuel": {
|
||||||
|
"petrol": "Petrol",
|
||||||
|
"diesel": "Diesel",
|
||||||
|
"cng": "CNG",
|
||||||
|
"lpg": "LPG",
|
||||||
|
"electric": "Electric",
|
||||||
|
"ethanol": "Ethanol"
|
||||||
|
},
|
||||||
|
"unit": {
|
||||||
|
"long": {
|
||||||
|
"litre": "Litre",
|
||||||
|
"gallon": "Gallon",
|
||||||
|
"kilowatthour": "Kilowatt Hour",
|
||||||
|
"kilogram": "Kilogram",
|
||||||
|
"usgallon": "US Gallon",
|
||||||
|
"minutes": "Minutes",
|
||||||
|
"kilometers": "Kilometers",
|
||||||
|
"miles": "Miles"
|
||||||
|
},
|
||||||
|
"short": {
|
||||||
|
"litre": "Lt",
|
||||||
|
"gallon": "Gal",
|
||||||
|
"kilowatthour": "KwH",
|
||||||
|
"kilogram": "Kg",
|
||||||
|
"usgallon": "US Gal",
|
||||||
|
"minutes": "Mins",
|
||||||
|
"kilometers": "Km",
|
||||||
|
"miles": "Mi"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"avgfillupqty": "Avg Fillup Qty",
|
||||||
|
"avgfillupexpense": "Avg Fillup Expense",
|
||||||
|
"avgfuelcost": "Avg Fuel Cost",
|
||||||
|
"per": "{0} per {1}",
|
||||||
|
"price": "Price",
|
||||||
|
"total": "Total",
|
||||||
|
"fulltank": "Tank Full",
|
||||||
|
"partialfillup": "Partial Fillup",
|
||||||
|
"getafulltank": "Did you get a full tank?",
|
||||||
|
"tankpartialfull": "Which do you track?",
|
||||||
|
"by": "By",
|
||||||
|
"expenses": "Expenses",
|
||||||
|
"expensetype": "Expense Type",
|
||||||
|
"noexpenses": "No Expenses so far",
|
||||||
|
"download": "Download",
|
||||||
|
"title": "Title",
|
||||||
|
"name": "Name",
|
||||||
|
"delete": "Delete",
|
||||||
|
"importdata": "Import data into Hammond",
|
||||||
|
"importdatadesc": "Choose from the following options to import data into Hammond",
|
||||||
|
"import": "Import",
|
||||||
|
"importcsv": "If you have been using {name} to store your vehicle data, export the CSV file from {name} and click here to import.",
|
||||||
|
"importgeneric": "Generic Fillups Import",
|
||||||
|
"importgenericdesc": "Fillups CSV import.",
|
||||||
|
"choosecsv": "Choose CSV",
|
||||||
|
"choosephoto": "Choose Photo",
|
||||||
|
"importsuccessfull": "Data Imported Successfully",
|
||||||
|
"importerror": "There was some issue with importing the file. Please check the error message",
|
||||||
|
"importfrom": "Import from {0}",
|
||||||
|
"stepstoimport": "Steps to import data from {name}",
|
||||||
|
"choosecsvimport": "Choose the {name} CSV and press the import button.",
|
||||||
|
"choosedatafile": "Choose the CSV file and then press the import button.",
|
||||||
|
"dontimportagain": "Make sure that you do not import the file again because that will create repeat entries.",
|
||||||
|
"checkpointsimportcsv": "Once you have checked all these points, just import the CSV below.",
|
||||||
|
"importhintunits": "Similiarly, make sure that the <u>Fuel Unit</u> and <u>Fuel Type</u> are correctly set in the Vehicle.",
|
||||||
|
"importhintcurrdist": "Make sure that the <u>Currency</u> and <u>Distance Unit</u> are set correctly in Hammond. Import will not autodetect Currency from the file but use the one set for the user.",
|
||||||
|
"importhintnickname": "Make sure that the Vehicle nickname in Hammond is exactly the same as the name on Fuelly CSV or the import will not work.",
|
||||||
|
"importhintvehiclecreated": "Make sure that you have already created the vehicles in Hammond platform.",
|
||||||
|
"importhintcreatecsv": "Export your data from {name} in the CSV format. Steps to do that can be found",
|
||||||
|
"importgenerichintdata": "Data must be in CSV format.",
|
||||||
|
"here": "here",
|
||||||
|
"unprocessedquickentries": "You have one quick entry to be processed. | You have {0} quick entries pending to be processed.",
|
||||||
|
"show": "Show",
|
||||||
|
"loginerror": "There was an error logging in to your account. {msg}",
|
||||||
|
"showunprocessed": "Show unprocessed only",
|
||||||
|
"unprocessed": "unprocessed",
|
||||||
|
"sitesettingdesc": "Update site level settings. These will be used as default values for new users.",
|
||||||
|
"settingdesc": "These will be used as default values whenever you create a new fillup or expense.",
|
||||||
|
"areyousure": "Are you sure you want to do this?",
|
||||||
|
"adduser": "Add User",
|
||||||
|
"usercreatedsuccessfully": "User Created Successfully",
|
||||||
|
"userdisabledsuccessfully": "User disabled successfully",
|
||||||
|
"userenabledsuccessfully": "User enabled successfully",
|
||||||
|
"role": "Role",
|
||||||
|
"created": "Created",
|
||||||
|
"createnewuser": "Create New User",
|
||||||
|
"cancel": "Cancel",
|
||||||
|
"novehicles": "It seems you have not yet created a vehicle in the system. Start by creating an entry for one of the vehicles you want to track.",
|
||||||
|
"processed": "Mark Processed",
|
||||||
|
"notfound": "Not Found",
|
||||||
|
"timeout": "The page timed out while loading. Are you sure you're still connected to\nthe Internet?",
|
||||||
|
"clicktoselect": "Click to select...",
|
||||||
|
"expenseby": "Expense by",
|
||||||
|
"selectvehicle": "Select a vehicle",
|
||||||
|
"expensedate": "Expense Date",
|
||||||
|
"totalamountpaid": "Total Amount Paid",
|
||||||
|
"fillmoredetails": "Fill more details",
|
||||||
|
"markquickentryprocessed": "Mark selected Quick Entry as processed",
|
||||||
|
"referquickentry": "Refer quick entry",
|
||||||
|
"deletequickentry": "This will delete this Quick Entry. This step cannot be reversed. Are you sure?",
|
||||||
|
"fuelunit": "Fuel Unit",
|
||||||
|
"fillingstation": "Filling Station Name",
|
||||||
|
"comments": "Comments",
|
||||||
|
"missfillupbefore": "Did you miss the fillup entry before this one?",
|
||||||
|
"missedfillup": "Missed Fillup",
|
||||||
|
"fillupdate": "Fillup Date",
|
||||||
|
"fillupsavedsuccessfully": "Fillup Saved Successfully",
|
||||||
|
"expensesavedsuccessfully": "Expense Saved Successfully",
|
||||||
|
"vehiclesavedsuccessfully": "Vehicle Saved Successfully",
|
||||||
|
"settingssavedsuccessfully": "Settings saved successfully",
|
||||||
|
"back": "Back",
|
||||||
|
"nickname": "Nickname",
|
||||||
|
"registration": "Registration",
|
||||||
|
"createvehicle": "Create Vehicle",
|
||||||
|
"make": "Make / Company",
|
||||||
|
"model": "Model",
|
||||||
|
"yearmanufacture": "Year of Manufacture",
|
||||||
|
"enginesize": "Engine Size (in cc)",
|
||||||
|
"mysqlconnstr": "Mysql Connection String",
|
||||||
|
"testconn": "Test Connection",
|
||||||
|
"migrate": "Migrate",
|
||||||
|
"init": {
|
||||||
|
"migrateclarkson": "Migrate from Clarkson",
|
||||||
|
"migrateclarksondesc": "If you have an existing Clarkson deployment and you want to migrate your data from that, press the following button.",
|
||||||
|
"freshinstall": "Fresh Install",
|
||||||
|
"freshinstalldesc": "If you want a fresh install of Hammond, press the following button.",
|
||||||
|
"clarkson": {
|
||||||
|
"desc": "<p>You need to make sure that this deployment of Hammond can access the MySQL database used by Clarkson.</p><p>If that is not directly possible, you can make a copy of that database somewhere accessible from this instance.</p><p>Once that is done, enter the connection string to the MySQL instance in the following format.</p><p>All the users imported from Clarkson will have their username as their email in Clarkson database and pasword set to<span class='' style='font-weight:bold'>hammond</span></p><code>user:pass@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local</code><br/><br/>",
|
||||||
|
"success": "We have successfully migrated the data from Clarkson. You will be redirected to the login screen shortly where you can login using your existing email and password : hammond"
|
||||||
|
},
|
||||||
|
"fresh": {
|
||||||
|
"setupadminuser": "Setup Admin Users",
|
||||||
|
"yourpassword": "Your Password",
|
||||||
|
"youremail": "Your Email",
|
||||||
|
"yourname": "Your Name",
|
||||||
|
"success": "You have been registered successfully. You will be redirected to the login screen shortly where you can login and start using the system."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"roles": {
|
||||||
|
"ADMIN": "ADMIN",
|
||||||
|
"USER": "USER"
|
||||||
|
},
|
||||||
|
"profile": "Profile",
|
||||||
|
"processedon": "Processed on",
|
||||||
|
"enable": "Enable",
|
||||||
|
"disable": "Disable",
|
||||||
|
"confirm": "Go Ahead",
|
||||||
|
"labelforfile": "Label for this file"
|
||||||
|
}
|
||||||
6
src/GarageApp/locales/index.tsx
Normal file
6
src/GarageApp/locales/index.tsx
Normal file
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const LANGUAGES = [
|
||||||
|
{ label: "Spanish", code: "es" },
|
||||||
|
{ label: "English", code: "en" },
|
||||||
|
{ label: "Italian", code: "it" },
|
||||||
|
];
|
||||||
|
|
||||||
1
src/GarageApp/logo.svg
Normal file
1
src/GarageApp/logo.svg
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
<svg id="Bun" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 70"><title>Bun Logo</title><path id="Shadow" d="M71.09,20.74c-.16-.17-.33-.34-.5-.5s-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5-.33-.34-.5-.5A26.46,26.46,0,0,1,75.5,35.7c0,16.57-16.82,30.05-37.5,30.05-11.58,0-21.94-4.23-28.83-10.86l.5.5.5.5.5.5.5.5.5.5.5.5.5.5C19.55,65.3,30.14,69.75,42,69.75c20.68,0,37.5-13.48,37.5-30C79.5,32.69,76.46,26,71.09,20.74Z"/><g id="Body"><path id="Background" d="M73,35.7c0,15.21-15.67,27.54-35,27.54S3,50.91,3,35.7C3,26.27,9,17.94,18.22,13S33.18,3,38,3s8.94,4.13,19.78,10C67,17.94,73,26.27,73,35.7Z" style="fill:#fbf0df"/><path id="Bottom_Shadow" data-name="Bottom Shadow" d="M73,35.7a21.67,21.67,0,0,0-.8-5.78c-2.73,33.3-43.35,34.9-59.32,24.94A40,40,0,0,0,38,63.24C57.3,63.24,73,50.89,73,35.7Z" style="fill:#f6dece"/><path id="Light_Shine" data-name="Light Shine" d="M24.53,11.17C29,8.49,34.94,3.46,40.78,3.45A9.29,9.29,0,0,0,38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7c0,.4,0,.8,0,1.19C9.06,15.48,20.07,13.85,24.53,11.17Z" style="fill:#fffefc"/><path id="Top" d="M35.12,5.53A16.41,16.41,0,0,1,29.49,18c-.28.25-.06.73.3.59,3.37-1.31,7.92-5.23,6-13.14C35.71,5,35.12,5.12,35.12,5.53Zm2.27,0A16.24,16.24,0,0,1,39,19c-.12.35.31.65.55.36C41.74,16.56,43.65,11,37.93,5,37.64,4.74,37.19,5.14,37.39,5.49Zm2.76-.17A16.42,16.42,0,0,1,47,17.12a.33.33,0,0,0,.65.11c.92-3.49.4-9.44-7.17-12.53C40.08,4.54,39.82,5.08,40.15,5.32ZM21.69,15.76a16.94,16.94,0,0,0,10.47-9c.18-.36.75-.22.66.18-1.73,8-7.52,9.67-11.12,9.45C21.32,16.4,21.33,15.87,21.69,15.76Z" style="fill:#ccbea7;fill-rule:evenodd"/><path id="Outline" d="M38,65.75C17.32,65.75.5,52.27.5,35.7c0-10,6.18-19.33,16.53-24.92,3-1.6,5.57-3.21,7.86-4.62,1.26-.78,2.45-1.51,3.6-2.19C32,1.89,35,.5,38,.5s5.62,1.2,8.9,3.14c1,.57,2,1.19,3.07,1.87,2.49,1.54,5.3,3.28,9,5.27C69.32,16.37,75.5,25.69,75.5,35.7,75.5,52.27,58.68,65.75,38,65.75ZM38,3c-2.42,0-5,1.25-8.25,3.13-1.13.66-2.3,1.39-3.54,2.15-2.33,1.44-5,3.07-8,4.7C8.69,18.13,3,26.62,3,35.7,3,50.89,18.7,63.25,38,63.25S73,50.89,73,35.7C73,26.62,67.31,18.13,57.78,13,54,11,51.05,9.12,48.66,7.64c-1.09-.67-2.09-1.29-3-1.84C42.63,4,40.42,3,38,3Z"/></g><g id="Mouth"><g id="Background-2" data-name="Background"><path d="M45.05,43a8.93,8.93,0,0,1-2.92,4.71,6.81,6.81,0,0,1-4,1.88A6.84,6.84,0,0,1,34,47.71,8.93,8.93,0,0,1,31.12,43a.72.72,0,0,1,.8-.81H44.26A.72.72,0,0,1,45.05,43Z" style="fill:#b71422"/></g><g id="Tongue"><path id="Background-3" data-name="Background" d="M34,47.79a6.91,6.91,0,0,0,4.12,1.9,6.91,6.91,0,0,0,4.11-1.9,10.63,10.63,0,0,0,1-1.07,6.83,6.83,0,0,0-4.9-2.31,6.15,6.15,0,0,0-5,2.78C33.56,47.4,33.76,47.6,34,47.79Z" style="fill:#ff6164"/><path id="Outline-2" data-name="Outline" d="M34.16,47a5.36,5.36,0,0,1,4.19-2.08,6,6,0,0,1,4,1.69c.23-.25.45-.51.66-.77a7,7,0,0,0-4.71-1.93,6.36,6.36,0,0,0-4.89,2.36A9.53,9.53,0,0,0,34.16,47Z"/></g><path id="Outline-3" data-name="Outline" d="M38.09,50.19a7.42,7.42,0,0,1-4.45-2,9.52,9.52,0,0,1-3.11-5.05,1.2,1.2,0,0,1,.26-1,1.41,1.41,0,0,1,1.13-.51H44.26a1.44,1.44,0,0,1,1.13.51,1.19,1.19,0,0,1,.25,1h0a9.52,9.52,0,0,1-3.11,5.05A7.42,7.42,0,0,1,38.09,50.19Zm-6.17-7.4c-.16,0-.2.07-.21.09a8.29,8.29,0,0,0,2.73,4.37A6.23,6.23,0,0,0,38.09,49a6.28,6.28,0,0,0,3.65-1.73,8.3,8.3,0,0,0,2.72-4.37.21.21,0,0,0-.2-.09Z"/></g><g id="Face"><ellipse id="Right_Blush" data-name="Right Blush" cx="53.22" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><ellipse id="Left_Bluch" data-name="Left Bluch" cx="22.95" cy="40.18" rx="5.85" ry="3.44" style="fill:#febbd0"/><path id="Eyes" d="M25.7,38.8a5.51,5.51,0,1,0-5.5-5.51A5.51,5.51,0,0,0,25.7,38.8Zm24.77,0A5.51,5.51,0,1,0,45,33.29,5.5,5.5,0,0,0,50.47,38.8Z" style="fill-rule:evenodd"/><path id="Iris" d="M24,33.64a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,24,33.64Zm24.77,0a2.07,2.07,0,1,0-2.06-2.07A2.07,2.07,0,0,0,48.75,33.64Z" style="fill:#fff;fill-rule:evenodd"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 3.8 KiB |
8
src/GarageApp/react.svg
Normal file
8
src/GarageApp/react.svg
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-11.5 -10.23174 23 20.46348">
|
||||||
|
<circle cx="0" cy="0" r="2.05" fill="#61dafb"/>
|
||||||
|
<g stroke="#61dafb" stroke-width="1" fill="none">
|
||||||
|
<ellipse rx="11" ry="4.2"/>
|
||||||
|
<ellipse rx="11" ry="4.2" transform="rotate(60)"/>
|
||||||
|
<ellipse rx="11" ry="4.2" transform="rotate(120)"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 338 B |
|
|
@ -1,11 +1,28 @@
|
||||||
import { serve } from "bun";
|
import { serve } from "bun";
|
||||||
import index from "./index.html";
|
import index from "./index.html";
|
||||||
|
import wisher from "./wisher/index.html"
|
||||||
|
import GarageApp from "./GarageApp/index.html"
|
||||||
|
import locales from "./GarageApp/locales/index"
|
||||||
|
|
||||||
const server = serve({
|
const server = serve({
|
||||||
routes: {
|
routes: {
|
||||||
// Serve index.html for all unmatched routes.
|
// Serve index.html for all unmatched routes.
|
||||||
"/*": index,
|
"/*": index,
|
||||||
|
|
||||||
|
"/wisher": wisher,
|
||||||
|
"/wisher/*": wisher,
|
||||||
|
|
||||||
|
"/GarageApp": GarageApp,
|
||||||
|
"/GarageApp/*": GarageApp,
|
||||||
|
"/GarageApp/locales/:lng/translation.json": async req => {
|
||||||
|
const lng = req.params.lng;
|
||||||
|
const ns = req.params.ns;
|
||||||
|
const path = `src/GarageApp/locales/${lng}/translation.json`
|
||||||
|
console.log(`${lng}, ${ns}`);
|
||||||
|
console.log(process.cwd());
|
||||||
|
return new Response(Bun.file(path))
|
||||||
|
},
|
||||||
|
|
||||||
"/api/hello": {
|
"/api/hello": {
|
||||||
async GET(req) {
|
async GET(req) {
|
||||||
return Response.json({
|
return Response.json({
|
||||||
|
|
|
||||||
BIN
src/wisher/.list.tsx.swp
Normal file
BIN
src/wisher/.list.tsx.swp
Normal file
Binary file not shown.
13
src/wisher/index.html
Normal file
13
src/wisher/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="../logo.svg" />
|
||||||
|
<title>Wisher with Bun + React</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="./list.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
61
src/wisher/list.tsx
Normal file
61
src/wisher/list.tsx
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import createRoot from "react-dom/client";
|
||||||
|
|
||||||
|
export function List() {
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("https://dummy.restapiexample.com/api/v1/employees")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(
|
||||||
|
(result) => {
|
||||||
|
setIsLoaded(true);
|
||||||
|
setItems(result.data);
|
||||||
|
console.log(result);
|
||||||
|
},
|
||||||
|
// Note: it's important to handle errors here
|
||||||
|
// instead of a catch() block so that we don't swallow
|
||||||
|
// exceptions from actual bugs in components.
|
||||||
|
(error) => {
|
||||||
|
setIsLoaded(true);
|
||||||
|
setError(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
if (error) {
|
||||||
|
return <>{error.message}</>;
|
||||||
|
} else if (!isLoaded) {
|
||||||
|
return <>loading...</>;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
/* here we map over the element and display each item as a card */
|
||||||
|
<div className="wrapper">
|
||||||
|
<ul className="card-grid">
|
||||||
|
{items.map((item) => (
|
||||||
|
<li>
|
||||||
|
<article className="card" key={item.id}>
|
||||||
|
<div className="card-content">
|
||||||
|
<h2 className="card-name">{item.employee_name}</h2>
|
||||||
|
<ol className="card-list">
|
||||||
|
<li>
|
||||||
|
Salary:{" "}
|
||||||
|
<span>{item.employee_salary}</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Age: <span>{item.employee_age}</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>,
|
||||||
|
<div className="code">
|
||||||
|
<h2> {items.print} </h2>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/wisher/list.txt
Normal file
57
src/wisher/list.txt
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import createRoot from "react-dom/client";
|
||||||
|
|
||||||
|
export function List() {
|
||||||
|
const [error, setError] = useState(null);
|
||||||
|
const [isLoaded, setIsLoaded] = useState(false);
|
||||||
|
const [items, setItems] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fetch("https://dummy.restapiexample.com/api/v1/employees")
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then(
|
||||||
|
(result) => {
|
||||||
|
setIsLoaded(true);
|
||||||
|
setItems(result);
|
||||||
|
},
|
||||||
|
// Note: it's important to handle errors here
|
||||||
|
// instead of a catch() block so that we don't swallow
|
||||||
|
// exceptions from actual bugs in components.
|
||||||
|
(error) => {
|
||||||
|
setIsLoaded(true);
|
||||||
|
setError(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
if (error) {
|
||||||
|
return <>{error.message}</>;
|
||||||
|
} else if (!isLoaded) {
|
||||||
|
return <>loading...</>;
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
/* here we map over the element and display each item as a card */
|
||||||
|
<div className="wrapper">
|
||||||
|
<ul className="card-grid">
|
||||||
|
{items.map((item) => (
|
||||||
|
<li>
|
||||||
|
<article className="card" key={item.id}>
|
||||||
|
<div className="card-content">
|
||||||
|
<h2 className="card-name">{item.emplyee_name}</h2>
|
||||||
|
<ol className="card-list">
|
||||||
|
<li>
|
||||||
|
Salary:{" "}
|
||||||
|
<span>{item.employee_salary}</span>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
Age: <span>{item.employee_age}</span>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue