diff --git a/fxmanifest.lua b/fxmanifest.lua
index a3417ea..b0fdcf9 100644
--- a/fxmanifest.lua
+++ b/fxmanifest.lua
@@ -7,6 +7,8 @@ version '1.9.0'
url 'https://github.com/overextended/oxmysql'
author 'overextended'
+ui_page 'src/ui/public/index.html'
+
dependencies {
'/server:5104',
}
@@ -15,4 +17,9 @@ server_scripts {
'dist/server/build.js',
}
+files {
+ 'src/ui/public/index.html',
+ 'src/ui/public/**/*'
+}
+
provide 'mysql-async'
diff --git a/src/ui/.gitignore b/src/ui/.gitignore
new file mode 100644
index 0000000..da93220
--- /dev/null
+++ b/src/ui/.gitignore
@@ -0,0 +1,4 @@
+/node_modules/
+/public/build/
+
+.DS_Store
diff --git a/src/ui/package.json b/src/ui/package.json
new file mode 100644
index 0000000..e22d758
--- /dev/null
+++ b/src/ui/package.json
@@ -0,0 +1,30 @@
+{
+ "name": "svelte-app",
+ "version": "1.0.0",
+ "private": true,
+ "scripts": {
+ "build": "rollup -c",
+ "dev": "rollup -c -w",
+ "start": "sirv public --no-clear"
+ },
+ "devDependencies": {
+ "@rollup/plugin-commonjs": "^17.0.0",
+ "@rollup/plugin-node-resolve": "^11.0.0",
+ "@rollup/plugin-typescript": "^8.0.0",
+ "@tsconfig/svelte": "^2.0.0",
+ "rollup": "^2.3.4",
+ "rollup-plugin-css-only": "^3.1.0",
+ "rollup-plugin-livereload": "^2.0.0",
+ "rollup-plugin-svelte": "^7.0.0",
+ "rollup-plugin-terser": "^7.0.0",
+ "svelte": "^3.0.0",
+ "svelte-check": "^2.0.0",
+ "svelte-preprocess": "^4.0.0",
+ "tslib": "^2.0.0",
+ "typescript": "^4.0.0"
+ },
+ "dependencies": {
+ "sirv-cli": "^1.0.0",
+ "svelte-spa-router": "^3.2.0"
+ }
+}
diff --git a/src/ui/public/global.css b/src/ui/public/global.css
new file mode 100644
index 0000000..a94d91a
--- /dev/null
+++ b/src/ui/public/global.css
@@ -0,0 +1,11 @@
+@import url('https://fonts.googleapis.com/css2?family=Inter&display=swap');
+
+* {
+ margin: 0;
+ padding: 0;
+ text-decoration: none;
+}
+
+body {
+ height: 100vh;
+}
diff --git a/src/ui/public/index.html b/src/ui/public/index.html
new file mode 100644
index 0000000..bd206a3
--- /dev/null
+++ b/src/ui/public/index.html
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+ Svelte app
+
+
+
+
+
+
+
+
+
diff --git a/src/ui/rollup.config.js b/src/ui/rollup.config.js
new file mode 100644
index 0000000..c584614
--- /dev/null
+++ b/src/ui/rollup.config.js
@@ -0,0 +1,87 @@
+import svelte from "rollup-plugin-svelte";
+import commonjs from "@rollup/plugin-commonjs";
+import resolve from "@rollup/plugin-node-resolve";
+import livereload from "rollup-plugin-livereload";
+import { terser } from "rollup-plugin-terser";
+import sveltePreprocess from "svelte-preprocess";
+import typescript from "@rollup/plugin-typescript";
+import css from "rollup-plugin-css-only";
+
+const production = !process.env.ROLLUP_WATCH;
+
+function serve() {
+ let server;
+
+ function toExit() {
+ if (server) server.kill(0);
+ }
+
+ return {
+ writeBundle() {
+ if (server) return;
+ server = require("child_process").spawn(
+ "npm",
+ ["run", "start", "--", "--dev"],
+ {
+ stdio: ["ignore", "inherit", "inherit"],
+ shell: true,
+ }
+ );
+
+ process.on("SIGTERM", toExit);
+ process.on("exit", toExit);
+ },
+ };
+}
+
+export default {
+ input: "src/main.ts",
+ output: {
+ sourcemap: false,
+ format: "iife",
+ name: "app",
+ file: "public/build/bundle.js",
+ },
+ plugins: [
+ svelte({
+ preprocess: sveltePreprocess({ sourceMap: !production }),
+ compilerOptions: {
+ // enable run-time checks when not in production
+ dev: !production,
+ },
+ }),
+ // we'll extract any component CSS out into
+ // a separate file - better for performance
+ css({ output: "bundle.css" }),
+
+ // If you have external dependencies installed from
+ // npm, you'll most likely need these plugins. In
+ // some cases you'll need additional configuration -
+ // consult the documentation for details:
+ // https://github.com/rollup/plugins/tree/master/packages/commonjs
+ resolve({
+ browser: true,
+ dedupe: ["svelte"],
+ }),
+ commonjs(),
+ typescript({
+ sourceMap: !production,
+ inlineSources: !production,
+ }),
+
+ // In dev mode, call `npm run start` once
+ // the bundle has been generated
+ !production && serve(),
+
+ // Watch the `public` directory and refresh the
+ // browser on changes when not in production
+ !production && livereload("public"),
+
+ // If we're building for production (npm run build
+ // instead of npm run dev), minify
+ production && terser(),
+ ],
+ watch: {
+ clearScreen: false,
+ },
+};
diff --git a/src/ui/src/App.svelte b/src/ui/src/App.svelte
new file mode 100644
index 0000000..7614e93
--- /dev/null
+++ b/src/ui/src/App.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/src/ui/src/components/Main.svelte b/src/ui/src/components/Main.svelte
new file mode 100644
index 0000000..2bfc574
--- /dev/null
+++ b/src/ui/src/components/Main.svelte
@@ -0,0 +1,49 @@
+
+
+
+
+
diff --git a/src/ui/src/components/menu/NavMenu.svelte b/src/ui/src/components/menu/NavMenu.svelte
new file mode 100644
index 0000000..5d04a15
--- /dev/null
+++ b/src/ui/src/components/menu/NavMenu.svelte
@@ -0,0 +1,61 @@
+
+
+
+
+ {#each executingResources as resource}
+
+ {/each}
+
+
+
diff --git a/src/ui/src/components/routes/Home.svelte b/src/ui/src/components/routes/Home.svelte
new file mode 100644
index 0000000..974b469
--- /dev/null
+++ b/src/ui/src/components/routes/Home.svelte
@@ -0,0 +1 @@
+Some data here
diff --git a/src/ui/src/components/routes/ResourceData.svelte b/src/ui/src/components/routes/ResourceData.svelte
new file mode 100644
index 0000000..693bee1
--- /dev/null
+++ b/src/ui/src/components/routes/ResourceData.svelte
@@ -0,0 +1,38 @@
+
+
+
+ {resource}
+ {queryData}
+
diff --git a/src/ui/src/global.d.ts b/src/ui/src/global.d.ts
new file mode 100644
index 0000000..1a25456
--- /dev/null
+++ b/src/ui/src/global.d.ts
@@ -0,0 +1 @@
+///
diff --git a/src/ui/src/main.ts b/src/ui/src/main.ts
new file mode 100644
index 0000000..83bc6ab
--- /dev/null
+++ b/src/ui/src/main.ts
@@ -0,0 +1,7 @@
+import App from "./App.svelte";
+
+const app = new App({
+ target: document.body,
+});
+
+export default app;
diff --git a/src/ui/src/providers/VisibilityProvider.svelte b/src/ui/src/providers/VisibilityProvider.svelte
new file mode 100644
index 0000000..582c90b
--- /dev/null
+++ b/src/ui/src/providers/VisibilityProvider.svelte
@@ -0,0 +1,35 @@
+
+
+
+ {#if isVisible}
+
+ {/if}
+
diff --git a/src/ui/src/store/stores.ts b/src/ui/src/store/stores.ts
new file mode 100644
index 0000000..508d4d9
--- /dev/null
+++ b/src/ui/src/store/stores.ts
@@ -0,0 +1,4 @@
+import { writable } from "svelte/store";
+
+export const visibility = writable(false);
+export const currentResource = writable('');
\ No newline at end of file
diff --git a/src/ui/src/utils/debugData.ts b/src/ui/src/utils/debugData.ts
new file mode 100644
index 0000000..aea5370
--- /dev/null
+++ b/src/ui/src/utils/debugData.ts
@@ -0,0 +1,30 @@
+import {isEnvBrowser} from "./misc";
+
+interface DebugEvent {
+ action: string;
+ data: T;
+}
+
+/**
+ * Emulates dispatching an event using SendNuiMessage in the lua scripts.
+ * This is used when developing in browser
+ *
+ * @param events - The event you want to cover
+ * @param timer - How long until it should trigger (ms)
+ */
+export const debugData = (events: DebugEvent
[], timer = 1000): void => {
+ if (isEnvBrowser()) {
+ for (const event of events) {
+ setTimeout(() => {
+ window.dispatchEvent(
+ new MessageEvent("message", {
+ data: {
+ action: event.action,
+ data: event.data,
+ },
+ })
+ );
+ }, timer);
+ }
+ }
+};
\ No newline at end of file
diff --git a/src/ui/src/utils/fetchNui.ts b/src/ui/src/utils/fetchNui.ts
new file mode 100644
index 0000000..95c2794
--- /dev/null
+++ b/src/ui/src/utils/fetchNui.ts
@@ -0,0 +1,27 @@
+/**
+* @param eventName - The endpoint eventname to target
+* @param data - Data you wish to send in the NUI Callback
+*
+* @return returnData - A promise for the data sent back by the NuiCallbacks CB argument
+*/
+
+export async function fetchNui(
+ eventName: string,
+ data: unknown = {}
+): Promise {
+ const options = {
+ method: "post",
+ headers: {
+ "Content-Type": "application/json; charset=UTF-8",
+ },
+ body: JSON.stringify(data),
+ };
+
+ const resourceName = (window as any).GetParentResourceName
+ ? (window as any).GetParentResourceName()
+ : "nui-frame-app";
+
+ const resp = await fetch(`https://${resourceName}/${eventName}`, options);
+
+ return await resp.json();
+}
diff --git a/src/ui/src/utils/misc.ts b/src/ui/src/utils/misc.ts
new file mode 100644
index 0000000..32eae35
--- /dev/null
+++ b/src/ui/src/utils/misc.ts
@@ -0,0 +1 @@
+export const isEnvBrowser = (): boolean => !(window as any).invokeNative;
diff --git a/src/ui/src/utils/useNuiEvent.ts b/src/ui/src/utils/useNuiEvent.ts
new file mode 100644
index 0000000..e1d8d2e
--- /dev/null
+++ b/src/ui/src/utils/useNuiEvent.ts
@@ -0,0 +1,31 @@
+import { onMount, onDestroy } from "svelte";
+
+interface NuiMessage {
+ action: string;
+ data: T;
+}
+
+/**
+ * A function that manage events listeners for receiving data from the client scripts
+ * @param action The specific `action` that should be listened for.
+ * @param handler The callback function that will handle data relayed by this function
+ *
+ * @example
+ * useNuiEvent<{visibility: true, wasVisible: 'something'}>('setVisible', (data) => {
+ * // whatever logic you want
+ * })
+ *
+ **/
+
+export function useNuiEvent(
+ action: string,
+ handler: (data: T) => void
+) {
+ const eventListener = (event: MessageEvent>) => {
+ const { action: eventAction, data } = event.data;
+
+ eventAction === action && handler(data);
+ };
+ onMount(() => window.addEventListener("message", eventListener));
+ onDestroy(() => window.removeEventListener("message", eventListener));
+}
diff --git a/src/ui/tsconfig.json b/src/ui/tsconfig.json
new file mode 100644
index 0000000..8139637
--- /dev/null
+++ b/src/ui/tsconfig.json
@@ -0,0 +1,13 @@
+{
+ "extends": "@tsconfig/svelte/tsconfig.json",
+
+ "compilerOptions": {
+ "strict": true,
+ "allowJs": true,
+ "allowSyntheticDefaultImports": true,
+ "noFallthroughCasesInSwitch": true
+ },
+
+ "include": ["src/**/*"],
+ "exclude": ["node_modules/*", "__sapper__/*", "public/*"]
+}