From e6d92f27b3be810eec95f28295711a4e30fe0aa2 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 18 Jun 2026 17:00:00 +0200 Subject: [PATCH] Migrate JavaScript to TypeScript with full type safety - Rename all .js/.jsx files to .ts/.tsx across resources/js and theme dirs - Add TypeScript 6.0 with strict mode, tsconfig.json - Add type definitions for Inertia page props, Alpine.js, Turbolinks - Update vite.config.js entries to .ts/.tsx extensions - Update all Blade @vite() calls to match new .ts/.tsx entry points - Add TypeScript ESLint config (replacing unused Vue plugin) - Add @types/react, @types/react-dom, @types/lodash - Add typecheck script and integrate into check pipeline - Full tsc --noEmit, ESLint, and production build pass cleanly --- eslint.config.js | 25 ++- package.json | 19 +- public/build/manifest.json | 106 +++++++-- resources/js/bootstrap.js | 34 --- resources/js/bootstrap.ts | 6 + resources/js/{global.js => global.ts} | 0 resources/js/pages/Home.jsx | 31 --- resources/js/pages/Home.tsx | 43 ++++ resources/js/pages/Index.jsx | 119 ---------- resources/js/pages/Index.tsx | 206 ++++++++++++++++++ resources/js/ssr.jsx | 12 - resources/js/ssr.tsx | 16 ++ resources/js/types.d.ts | 31 +++ resources/themes/atom/js/app.js | 25 --- resources/themes/atom/js/app.ts | 18 ++ resources/themes/atom/js/bootstrap.js | 34 --- resources/themes/atom/js/bootstrap.ts | 7 + .../atom/js/components/ArticleReactions.js | 131 ----------- .../atom/js/components/ArticleReactions.ts | 147 +++++++++++++ .../{AtomSliders.js => AtomSliders.ts} | 7 +- .../{ThemeSwitcher.js => ThemeSwitcher.ts} | 15 +- .../themes/atom/views/client/nitro.blade.php | 2 +- .../themes/atom/views/layouts/app.blade.php | 2 +- .../themes/atom/views/layouts/guest.blade.php | 2 +- .../themes/atom/views/maintenance.blade.php | 2 +- resources/themes/atom/vite.config.js | 8 +- resources/themes/dusk/js/app.js | 40 ---- resources/themes/dusk/js/app.ts | 36 +++ resources/themes/dusk/js/bootstrap.js | 31 --- resources/themes/dusk/js/bootstrap.ts | 4 + .../dusk/js/components/ArticleReactions.js | 131 ----------- .../dusk/js/components/ArticleReactions.ts | 147 +++++++++++++ .../themes/dusk/views/client/nitro.blade.php | 2 +- .../themes/dusk/views/layouts/app.blade.php | 2 +- .../themes/dusk/views/maintenance.blade.php | 2 +- resources/themes/dusk/vite.config.js | 8 +- resources/views/app.blade.php | 2 +- .../views/layouts/installation.blade.php | 2 +- tsconfig.json | 30 +++ vite.config.js | 4 +- yarn.lock | 177 ++++++++++++--- 41 files changed, 986 insertions(+), 680 deletions(-) delete mode 100755 resources/js/bootstrap.js create mode 100755 resources/js/bootstrap.ts rename resources/js/{global.js => global.ts} (100%) delete mode 100755 resources/js/pages/Home.jsx create mode 100755 resources/js/pages/Home.tsx delete mode 100755 resources/js/pages/Index.jsx create mode 100755 resources/js/pages/Index.tsx delete mode 100755 resources/js/ssr.jsx create mode 100755 resources/js/ssr.tsx create mode 100644 resources/js/types.d.ts delete mode 100755 resources/themes/atom/js/app.js create mode 100755 resources/themes/atom/js/app.ts delete mode 100755 resources/themes/atom/js/bootstrap.js create mode 100755 resources/themes/atom/js/bootstrap.ts delete mode 100755 resources/themes/atom/js/components/ArticleReactions.js create mode 100755 resources/themes/atom/js/components/ArticleReactions.ts rename resources/themes/atom/js/components/{AtomSliders.js => AtomSliders.ts} (83%) rename resources/themes/atom/js/components/{ThemeSwitcher.js => ThemeSwitcher.ts} (71%) delete mode 100755 resources/themes/dusk/js/app.js create mode 100755 resources/themes/dusk/js/app.ts delete mode 100755 resources/themes/dusk/js/bootstrap.js create mode 100755 resources/themes/dusk/js/bootstrap.ts delete mode 100755 resources/themes/dusk/js/components/ArticleReactions.js create mode 100755 resources/themes/dusk/js/components/ArticleReactions.ts create mode 100644 tsconfig.json diff --git a/eslint.config.js b/eslint.config.js index 5f2f610..a0ec258 100755 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,5 +1,6 @@ import globals from "globals"; -import pluginVue from "eslint-plugin-vue"; +import tseslint from "@typescript-eslint/eslint-plugin"; +import tsParser from "@typescript-eslint/parser"; export default [ { @@ -11,22 +12,32 @@ export default [ "build/**", ], }, - ...pluginVue.configs["flat/essential"], { + files: ["**/*.{ts,tsx}"], languageOptions: { + parser: tsParser, + parserOptions: { + ecmaVersion: "latest", + sourceType: "module", + ecmaFeatures: { + jsx: true, + }, + }, globals: { ...globals.browser, ...globals.node, - Alpine: "readonly", - $: "readonly", - jQuery: "readonly", }, }, + plugins: { + "@typescript-eslint": tseslint, + }, rules: { "no-console": "warn", "no-debugger": "warn", - "vue/multi-word-component-names": "off", - "vue/no-v-html": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_" }, + ], }, }, ]; diff --git a/package.json b/package.json index 88e28ef..7634b12 100755 --- a/package.json +++ b/package.json @@ -8,16 +8,17 @@ "build:atom": "vite build --config resources/themes/atom/vite.config.js && php artisan optimize:clear && php artisan optimize && chown -R www-data:www-data public/build", "build:dusk": "vite build --config resources/themes/dusk/vite.config.js && php artisan optimize:clear && php artisan optimize && chown -R www-data:www-data public/build", "build:all": "vite build --config resources/themes/atom/vite.config.js && vite build --config resources/themes/dusk/vite.config.js && php artisan optimize:clear && php artisan optimize && chown -R www-data:www-data public/build", + "typecheck": "tsc --noEmit", "preview": "vite preview", - "format": "prettier \"resources/**/*.{js,ts,vue,blade}\" --ignore-unknown --write", - "format:check": "prettier \"resources/**/*.{js,ts,vue,blade.php}\" --check", - "lint": "eslint resources/js/", - "lint:fix": "eslint resources/js/ --fix", + "format": "prettier \"resources/**/*.{js,ts,tsx,vue,blade}\" --ignore-unknown --write", + "format:check": "prettier \"resources/**/*.{js,ts,tsx,vue,blade.php}\" --check", + "lint": "eslint \"resources/js/\" --ext .ts,.tsx", + "lint:fix": "eslint \"resources/js/\" --ext .ts,.tsx --fix", "lint:css": "stylelint \"resources/**/*.css\"", "lint:css:fix": "stylelint \"resources/**/*.css\" --fix", "lint:all": "yarn lint && yarn lint:css", "lint:fix:all": "yarn lint:fix && yarn lint:fix:css", - "check": "yarn lint:all && yarn format:check", + "check": "yarn typecheck && yarn lint:all && yarn format:check", "check:php": "php -l app/ && php -l routes/ && php -l database/ && php -l resources/", "check:security": "composer audit && npm audit", "check:deps": "npm outdated --long --json || true", @@ -33,12 +34,17 @@ "@tailwindcss/forms": "0.5.11", "@tailwindcss/postcss": "4.3.0", "@tailwindcss/typography": "0.5.19", + "@types/lodash": "^4.17.24", + "@types/react": "^19.2.17", + "@types/react-dom": "^19.2.3", + "@typescript-eslint/eslint-plugin": "^8.61.1", + "@typescript-eslint/parser": "^8.61.1", "alpinejs": "3.15.12", "autoprefixer": "10.5.0", "axios": "^1.17.0", "esbuild": "^0.28.0", "eslint": "^10.4.1", - "eslint-plugin-vue": "10.9.2", + "globals": "^17.6.0", "laravel-vite-plugin": "^3.1.0", "lodash": "^4.18.1", "postcss": "8.5.15", @@ -49,6 +55,7 @@ "stylelint-config-standard": "^40.0.0", "tailwindcss": "4.3.0", "turbolinks": "5.2.0", + "typescript": "^6.0.3", "vite": "^8.0.16" }, "dependencies": { diff --git a/public/build/manifest.json b/public/build/manifest.json index f7d8067..7bc56e7 100644 --- a/public/build/manifest.json +++ b/public/build/manifest.json @@ -1,15 +1,26 @@ { - "_axios-Bb9VWCvi.js": { - "file": "assets/axios-Bb9VWCvi.js", + "_axios-BEkq_c61.js": { + "file": "assets/axios-BEkq_c61.js", "name": "axios", "imports": [ - "_chunk-QTnfLwEv.js" + "_chunk-b3L32Ng1.js" ] }, - "_chunk-QTnfLwEv.js": { - "file": "assets/chunk-QTnfLwEv.js", + "_chunk-b3L32Ng1.js": { + "file": "assets/chunk-b3L32Ng1.js", "name": "chunk" }, + "_swiper-Cjlszzo3.js": { + "file": "assets/swiper-Cjlszzo3.js", + "name": "swiper", + "css": [ + "assets/swiper-CrMA9oas.css" + ] + }, + "_swiper-CrMA9oas.css": { + "file": "assets/swiper-CrMA9oas.css", + "src": "_swiper-CrMA9oas.css" + }, "public/assets/images/background-dark.jpg": { "file": "assets/background-dark-BfkMu3-0.jpg", "src": "public/assets/images/background-dark.jpg" @@ -18,6 +29,18 @@ "file": "assets/background-light-CP7oKwVT.jpg", "src": "public/assets/images/background-light.jpg" }, + "public/assets/images/dusk/background_image.png": { + "file": "assets/background_image-BH7pVpv1.png", + "src": "public/assets/images/dusk/background_image.png" + }, + "public/assets/images/dusk/leaderboard_circle_image.png": { + "file": "assets/leaderboard_circle_image-BYkDVX69.png", + "src": "public/assets/images/dusk/leaderboard_circle_image.png" + }, + "public/assets/images/dusk/store_icon.png": { + "file": "assets/store_icon-B52tsSKO.png", + "src": "public/assets/images/dusk/store_icon.png" + }, "public/assets/images/icons/article.gif": { "file": "assets/article-CYhGsSKA.gif", "src": "public/assets/images/icons/article.gif" @@ -111,7 +134,7 @@ "src": "public/assets/images/profile/profile-bg.png" }, "resources/css/global.css": { - "file": "assets/global-DmKtm1TC.css", + "file": "assets/global-CwMfkl9f.css", "name": "global", "names": [ "global.css" @@ -146,27 +169,27 @@ "assets/community-Do_t1zw9.png" ] }, - "resources/js/global.js": { - "file": "assets/global-r22-sRCc.js", + "resources/js/global.ts": { + "file": "assets/global-B6tm4RcQ.js", "name": "global", - "src": "resources/js/global.js", + "src": "resources/js/global.ts", "isEntry": true, "imports": [ - "_chunk-QTnfLwEv.js", - "_axios-Bb9VWCvi.js" + "_chunk-b3L32Ng1.js", + "_axios-BEkq_c61.js" ] }, - "resources/js/ssr.jsx": { - "file": "assets/ssr-DdmZbD73.js", + "resources/js/ssr.tsx": { + "file": "assets/ssr-GVDc-G73.js", "name": "ssr", - "src": "resources/js/ssr.jsx", + "src": "resources/js/ssr.tsx", "isEntry": true, "imports": [ - "_chunk-QTnfLwEv.js" + "_chunk-b3L32Ng1.js" ] }, "resources/themes/atom/css/app.css": { - "file": "assets/app-DtTGSxkD.css", + "file": "assets/app-BPKvU7LK.css", "name": "app", "names": [ "app.css" @@ -201,17 +224,56 @@ "assets/community-Do_t1zw9.png" ] }, - "resources/themes/atom/js/app.js": { - "file": "assets/app-CAkt-7PZ.js", + "resources/themes/atom/js/app.ts": { + "file": "assets/app-evCrhLY1.js", "name": "app", - "src": "resources/themes/atom/js/app.js", + "src": "resources/themes/atom/js/app.ts", "isEntry": true, "imports": [ - "_chunk-QTnfLwEv.js", - "_axios-Bb9VWCvi.js" + "_chunk-b3L32Ng1.js", + "_swiper-Cjlszzo3.js", + "_axios-BEkq_c61.js" + ] + }, + "resources/themes/dusk/css/app.css": { + "file": "assets/app-CHSILL1f.css", + "name": "app", + "names": [ + "app.css" + ], + "src": "resources/themes/dusk/css/app.css", + "isEntry": true, + "assets": [ + "assets/background_image-BH7pVpv1.png", + "assets/feeds-BtHcJdHX.png", + "assets/chat-r5H1PnTg.png", + "assets/article-CYhGsSKA.gif", + "assets/lighthouse-BON6qnQ0.png", + "assets/store_icon-B52tsSKO.png", + "assets/catalog-D-956oDx.png", + "assets/inventory-BlHYLNGT.png", + "assets/due-chat-CeO4yxLu.png", + "assets/friends-BxpcKlvz.png", + "assets/credits-Dpg5Nmby.png", + "assets/duckets-CaGJI1Oy.png", + "assets/diamonds-BtfqKoQu.png", + "assets/trophy-gold-bbKmpkii.png", + "assets/trophy-silver-bGfHJkQ_.png", + "assets/trophy-bronze-CgV5j1MU.png", + "assets/leaderboard_circle_image-BYkDVX69.png" + ] + }, + "resources/themes/dusk/js/app.ts": { + "file": "assets/app-DKy1JARZ.js", + "name": "app", + "src": "resources/themes/dusk/js/app.ts", + "isEntry": true, + "imports": [ + "_swiper-Cjlszzo3.js", + "_axios-BEkq_c61.js" ], "css": [ - "assets/app-CeYfhhVD.css" + "assets/app-DU8Y3NnC.css" ] } } \ No newline at end of file diff --git a/resources/js/bootstrap.js b/resources/js/bootstrap.js deleted file mode 100755 index 34478d4..0000000 --- a/resources/js/bootstrap.js +++ /dev/null @@ -1,34 +0,0 @@ -import _ from "lodash"; -window._ = _; - -/** - * We'll load the axios HTTP library which allows us to easily issue requests - * to our Laravel back-end. This library automatically handles sending the - * CSRF token as a header based on the value of the "XSRF" token cookie. - */ - -import axios from "axios"; -window.axios = axios; - -window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; - -/** - * Echo exposes an expressive API for subscribing to channels and listening - * for events that are broadcast by Laravel. Echo and event broadcasting - * allows your team to easily build robust real-time web applications. - */ - -// import Echo from 'laravel-echo'; - -// import Pusher from 'pusher-js'; -// window.Pusher = Pusher; - -// window.Echo = new Echo({ -// broadcaster: 'pusher', -// key: import.meta.env.VITE_PUSHER_APP_KEY, -// wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`, -// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, -// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, -// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', -// enabledTransports: ['ws', 'wss'], -// }); diff --git a/resources/js/bootstrap.ts b/resources/js/bootstrap.ts new file mode 100755 index 0000000..11434e8 --- /dev/null +++ b/resources/js/bootstrap.ts @@ -0,0 +1,6 @@ +import _ from "lodash"; +import axios from "axios"; + +window._ = _; +window.axios = axios; +window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; diff --git a/resources/js/global.js b/resources/js/global.ts similarity index 100% rename from resources/js/global.js rename to resources/js/global.ts diff --git a/resources/js/pages/Home.jsx b/resources/js/pages/Home.jsx deleted file mode 100755 index e0bf985..0000000 --- a/resources/js/pages/Home.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Head } from '@inertiajs/react' - -export default function Home({ auth, hotelName }) { - return ( - <> - -
-
-

- Welkom bij {hotelName} -

-

- Dit is een Inertia.js pagina — zelfde layout, zelfde Tailwind, zelfde stijlen. -

-
- - Naar huis - -
-
-
- - ) -} diff --git a/resources/js/pages/Home.tsx b/resources/js/pages/Home.tsx new file mode 100755 index 0000000..34ce8c2 --- /dev/null +++ b/resources/js/pages/Home.tsx @@ -0,0 +1,43 @@ +import { Head } from "@inertiajs/react"; + +interface HomeProps { + auth: Record; + hotelName: string; +} + +export default function Home({ hotelName }: HomeProps) { + return ( + <> + +
+
+

+ Welkom bij {hotelName} +

+

+ Dit is een Inertia.js pagina — zelfde layout, zelfde Tailwind, + zelfde stijlen. +

+
+ + Naar huis + +
+
+
+ + ); +} diff --git a/resources/js/pages/Index.jsx b/resources/js/pages/Index.jsx deleted file mode 100755 index 4d3a3fa..0000000 --- a/resources/js/pages/Index.jsx +++ /dev/null @@ -1,119 +0,0 @@ -import { Head, usePage } from '@inertiajs/react' - -export default function Index({ articles, photos }) { - const { avatarImager } = usePage().props - - return ( - <> - - -
-
-
- - - {photos.length > 0 && ( -
-
-
-
-
-

Laatste foto's

-

Bekijk de mooiste momenten vastgelegd door gebruikers.

-
-
- - -
- )} -
- - ) -} diff --git a/resources/js/pages/Index.tsx b/resources/js/pages/Index.tsx new file mode 100755 index 0000000..d361847 --- /dev/null +++ b/resources/js/pages/Index.tsx @@ -0,0 +1,206 @@ +import { Head, usePage } from "@inertiajs/react"; + +interface User { + look: string; + username: string; +} + +interface Article { + id: number; + slug: string; + title: string; + image: string; + user?: User; +} + +interface Photo { + id: number; + url: string; + user?: User; +} + +interface IndexProps { + articles: Article[]; + photos: Photo[]; +} + +interface SharedProps extends Record { + avatarImager: string; +} + +export default function Index({ articles, photos }: IndexProps) { + const { avatarImager } = usePage().props; + + return ( + <> + + +
+
+
+
+
+
+

+ Laatste nieuws +

+

+ Blijf op de hoogte van het laatste hotel nieuws. +

+
+
+ + + + {photos.length > 0 && ( +
+
+
+
+
+

+ Laatste foto's +

+

+ Bekijk de mooiste momenten vastgelegd door gebruikers. +

+
+
+ + +
+ )} +
+ + ); +} diff --git a/resources/js/ssr.jsx b/resources/js/ssr.jsx deleted file mode 100755 index 49ff5b1..0000000 --- a/resources/js/ssr.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import { createInertiaApp } from '@inertiajs/react' -import { createRoot } from 'react-dom/client' - -createInertiaApp({ - resolve: name => { - const pages = import.meta.glob('./pages/**/*.jsx', { eager: true }) - return pages[`./pages/${name}.jsx`] - }, - setup({ el, App, props }) { - createRoot(el).render() - }, -}) diff --git a/resources/js/ssr.tsx b/resources/js/ssr.tsx new file mode 100755 index 0000000..9ceb0fc --- /dev/null +++ b/resources/js/ssr.tsx @@ -0,0 +1,16 @@ +import { createInertiaApp } from "@inertiajs/react"; +import { createRoot } from "react-dom/client"; +import type { ComponentType } from "react"; + +createInertiaApp({ + resolve: (name: string) => { + const pages = import.meta.glob<{ default: ComponentType }>( + "./pages/**/*.tsx", + { eager: true }, + ); + return pages[`./pages/${name}.tsx`]; + }, + setup({ el, App, props }) { + createRoot(el).render(); + }, +}); diff --git a/resources/js/types.d.ts b/resources/js/types.d.ts new file mode 100644 index 0000000..d1c49a9 --- /dev/null +++ b/resources/js/types.d.ts @@ -0,0 +1,31 @@ +declare module "alpinejs" { + interface Alpine { + data(name: string, callback: (...args: unknown[]) => Record): void; + plugin(plugin: unknown): void; + start(): void; + } + const Alpine: Alpine; + export default Alpine; +} + +declare module "@alpinejs/focus" { + const focus: unknown; + export default focus; +} + +declare module "turbolinks" { + interface TurbolinksStatic { + start(): void; + } + const Turbolinks: TurbolinksStatic; + export default Turbolinks; +} + +interface Window { + _: import("lodash").default; + axios: import("axios").AxiosStatic; + App: { + defaultReactions: string[]; + isAuthenticated: boolean; + }; +} diff --git a/resources/themes/atom/js/app.js b/resources/themes/atom/js/app.js deleted file mode 100755 index 46cd354..0000000 --- a/resources/themes/atom/js/app.js +++ /dev/null @@ -1,25 +0,0 @@ -import "./bootstrap"; -import "./external/flowbite"; - -import "swiper/css"; -import "swiper/css/pagination"; - -import Alpine from "alpinejs"; -import Focus from "@alpinejs/focus"; - -import ArticleReactions from "./components/ArticleReactions.js"; - -import ThemeSwitcher from "./components/ThemeSwitcher.js"; -import AtomSliders from "./components/AtomSliders.js"; - -ThemeSwitcher.init(); -ArticleReactions.init(); -AtomSliders.init(); -Alpine.plugin(Focus); -Alpine.start(); - -console.log( - "%cAtom CMS%c\n\nAtom CMS is a CMS for made for the community to enjoy. You can join our wonderful community at https://discord.gg/rX3aShUHdg\n\n", - "color: #14619c; -webkit-text-stroke: 2px black; font-size: 32px; font-weight: bold;", - "", -); diff --git a/resources/themes/atom/js/app.ts b/resources/themes/atom/js/app.ts new file mode 100755 index 0000000..4101c2f --- /dev/null +++ b/resources/themes/atom/js/app.ts @@ -0,0 +1,18 @@ +import "./bootstrap"; +import "./external/flowbite"; + +import "swiper/css"; +import "swiper/css/pagination"; + +import Alpine from "alpinejs"; +import Focus from "@alpinejs/focus"; + +import ArticleReactions from "./components/ArticleReactions"; +import ThemeSwitcher from "./components/ThemeSwitcher"; +import AtomSliders from "./components/AtomSliders"; + +ThemeSwitcher.init(); +ArticleReactions.init(); +AtomSliders.init(); +Alpine.plugin(Focus); +Alpine.start(); diff --git a/resources/themes/atom/js/bootstrap.js b/resources/themes/atom/js/bootstrap.js deleted file mode 100755 index 25ba4f6..0000000 --- a/resources/themes/atom/js/bootstrap.js +++ /dev/null @@ -1,34 +0,0 @@ -/** - * We'll load the axios HTTP library which allows us to easily issue requests - * to our Laravel back-end. This library automatically handles sending the - * CSRF token as a header based on the value of the "XSRF" token cookie. - */ - -import axios from "axios"; -import Turbolinks from "turbolinks"; - -window.axios = axios; -window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; - -Turbolinks.start(); - -/** - * Echo exposes an expressive API for subscribing to channels and listening - * for events that are broadcast by Laravel. Echo and event broadcasting - * allows your team to easily build robust real-time web applications. - */ - -// import Echo from 'laravel-echo'; - -// import Pusher from 'pusher-js'; -// window.Pusher = Pusher; - -// window.Echo = new Echo({ -// broadcaster: 'pusher', -// key: import.meta.env.VITE_PUSHER_APP_KEY, -// wsHost: import.meta.env.VITE_PUSHER_HOST ?? `ws-${import.meta.env.VITE_PUSHER_CLUSTER}.pusher.com`, -// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80, -// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443, -// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https', -// enabledTransports: ['ws', 'wss'], -// }); diff --git a/resources/themes/atom/js/bootstrap.ts b/resources/themes/atom/js/bootstrap.ts new file mode 100755 index 0000000..88f04f2 --- /dev/null +++ b/resources/themes/atom/js/bootstrap.ts @@ -0,0 +1,7 @@ +import axios from "axios"; +import Turbolinks from "turbolinks"; + +window.axios = axios; +window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest"; + +Turbolinks.start(); diff --git a/resources/themes/atom/js/components/ArticleReactions.js b/resources/themes/atom/js/components/ArticleReactions.js deleted file mode 100755 index 9163877..0000000 --- a/resources/themes/atom/js/components/ArticleReactions.js +++ /dev/null @@ -1,131 +0,0 @@ -import Alpine from "alpinejs"; - -const ArticleReactions = { - init() { - document.addEventListener("alpine:init", () => this.startComponent()); - }, - - startComponent() { - Alpine.data( - "reactions", - (myReactions = [], articleReactions = [], url = "") => ({ - url, - myReactions, - articleReactions, - allReactions: [], - isAuthenticated: false, - - init() { - this.treatArticleReactions(); - this.allReactions = window.App.defaultReactions; - this.isAuthenticated = window.App.isAuthenticated; - - this.dispatchFlowbiteEvent(); - }, - - treatArticleReactions() { - let articleReactions = this.articleReactions; - - this.articleReactions = []; - - Object.entries(articleReactions).forEach((reactionData) => { - let reactionName = reactionData[0], - reactions = Object.values(reactionData[1]); - - this.articleReactions.push({ - id: this.generateVirtualReactionId(reactionName), - name: reactionName, - count: reactions.length, - users: reactions.map((reaction) => reaction.user?.username ?? ""), - }); - }); - }, - - toggleReaction(reaction) { - if (!this.url.length || !this.isAuthenticated) return; - - axios.post(this.url, { reaction }).then((response) => { - if (!response.data.success) return; - - if (!response.data.added) { - this.removeReaction(reaction, response.data.username); - return; - } - - this.addReaction(reaction, response.data.username); - }); - }, - - addReaction(name, username) { - this.myReactions.push(name); - - let existingReaction = this.getReactionDataFromName(name); - - if (existingReaction) { - existingReaction.count++; - existingReaction.users.push(username); - return; - } - - this.articleReactions.push({ - id: this.generateVirtualReactionId(name), - name, - count: 1, - users: [username], - }); - - this.dispatchFlowbiteEvent(); - }, - - removeReaction(name, username) { - this.myReactions.splice(this.myReactions.indexOf(name), 1); - - let reactionData = this.getReactionDataFromName(name); - - if (reactionData.count > 1) { - reactionData.count--; - reactionData.users.splice(reactionData.users.indexOf(username), 1); - return; - } - - this.$nextTick(() => { - this.articleReactions.splice( - this.articleReactions.indexOf(reactionData), - 1, - ); - }); - }, - - generateVirtualReactionId(name) { - return name + Math.floor(Math.random() * 1000); - }, - - canAddReactionFromModal(name) { - return !this.userHasReaction(name) && !this.articleHasReaction(name); - }, - - userHasReaction(reaction) { - return this.myReactions.includes(reaction.name); - }, - - articleHasReaction(name) { - return typeof this.getReactionDataFromName(name) !== "undefined"; - }, - - getReactionDataFromName(name) { - return this.articleReactions.find( - (reaction) => reaction.name === name, - ); - }, - - dispatchFlowbiteEvent() { - this.$nextTick(() => - document.dispatchEvent(new CustomEvent("reactions:loaded")), - ); - }, - }), - ); - }, -}; - -export { ArticleReactions as default }; diff --git a/resources/themes/atom/js/components/ArticleReactions.ts b/resources/themes/atom/js/components/ArticleReactions.ts new file mode 100755 index 0000000..7ba0723 --- /dev/null +++ b/resources/themes/atom/js/components/ArticleReactions.ts @@ -0,0 +1,147 @@ +import Alpine from "alpinejs"; + +interface ReactionData { + id: string; + name: string; + count: number; + users: string[]; +} + +interface ReactionComponent extends Record { + url: string; + myReactions: string[]; + articleReactions: ReactionData[]; + allReactions: string[]; + isAuthenticated: boolean; + init(): void; + treatArticleReactions(rawReactions: Record): void; + toggleReaction(reaction: string): void; + addReaction(name: string, username: string): void; + removeReaction(name: string, username: string): void; + generateVirtualReactionId(name: string): string; + canAddReactionFromModal(name: string): boolean; + userHasReaction(reaction: ReactionData | string): boolean; + articleHasReaction(name: string): boolean; + getReactionDataFromName(name: string): ReactionData | undefined; + dispatchFlowbiteEvent(): void; + $nextTick: (callback: () => void) => void; +} + +const ArticleReactions = { + init() { + document.addEventListener("alpine:init", () => this.startComponent()); + }, + + startComponent() { + Alpine.data("reactions", (...args: unknown[]) => { + const [myReactions = [], , url = ""] = args as [string[], Record, string]; + const rawArticleReactions = args[1] as Record | undefined; + + return { + url, + myReactions: myReactions as string[], + articleReactions: [] as ReactionData[], + allReactions: [] as string[], + isAuthenticated: false, + + init(this: ReactionComponent) { + if (rawArticleReactions) { + this.treatArticleReactions(rawArticleReactions); + } + this.allReactions = window.App.defaultReactions; + this.isAuthenticated = window.App.isAuthenticated; + this.dispatchFlowbiteEvent(); + }, + + treatArticleReactions(this: ReactionComponent, raw: Record) { + const transformed: ReactionData[] = []; + + Object.entries(raw).forEach(([name, reactions]) => { + const values = Object.values(reactions); + transformed.push({ + id: this.generateVirtualReactionId(name), + name, + count: values.length, + users: values.map((r) => r.user?.username ?? ""), + }); + }); + + this.articleReactions = transformed; + }, + + toggleReaction(this: ReactionComponent, reaction: string) { + if (!this.url.length || !this.isAuthenticated) return; + + window.axios + .post(this.url, { reaction }) + .then((response: { data: { success: boolean; added: boolean; username: string } }) => { + if (!response.data.success) return; + if (!response.data.added) { + this.removeReaction(reaction, response.data.username); + return; + } + this.addReaction(reaction, response.data.username); + }); + }, + + addReaction(this: ReactionComponent, name: string, username: string) { + this.myReactions.push(name); + const existing = this.getReactionDataFromName(name); + if (existing) { + existing.count++; + existing.users.push(username); + return; + } + this.articleReactions.push({ + id: this.generateVirtualReactionId(name), + name, + count: 1, + users: [username], + }); + this.dispatchFlowbiteEvent(); + }, + + removeReaction(this: ReactionComponent, name: string, username: string) { + this.myReactions.splice(this.myReactions.indexOf(name), 1); + const data = this.getReactionDataFromName(name); + if (!data) return; + if (data.count > 1) { + data.count--; + data.users.splice(data.users.indexOf(username), 1); + return; + } + this.$nextTick(() => { + this.articleReactions.splice(this.articleReactions.indexOf(data), 1); + }); + }, + + generateVirtualReactionId(this: ReactionComponent, name: string): string { + return name + Math.floor(Math.random() * 1000); + }, + + canAddReactionFromModal(this: ReactionComponent, name: string): boolean { + return !this.userHasReaction(name) && !this.articleHasReaction(name); + }, + + userHasReaction(this: ReactionComponent, reaction: ReactionData | string): boolean { + const name = typeof reaction === "string" ? reaction : reaction.name; + return this.myReactions.includes(name); + }, + + articleHasReaction(this: ReactionComponent, name: string): boolean { + return typeof this.getReactionDataFromName(name) !== "undefined"; + }, + + getReactionDataFromName(this: ReactionComponent, name: string): ReactionData | undefined { + return this.articleReactions.find((r) => r.name === name); + }, + + dispatchFlowbiteEvent(this: ReactionComponent) { + this.$nextTick(() => document.dispatchEvent(new CustomEvent("reactions:loaded"))); + }, + } as ReactionComponent; + }); + }, +}; + +export { ArticleReactions as default }; diff --git a/resources/themes/atom/js/components/AtomSliders.js b/resources/themes/atom/js/components/AtomSliders.ts similarity index 83% rename from resources/themes/atom/js/components/AtomSliders.js rename to resources/themes/atom/js/components/AtomSliders.ts index afbf1fc..be5a2de 100755 --- a/resources/themes/atom/js/components/AtomSliders.js +++ b/resources/themes/atom/js/components/AtomSliders.ts @@ -1,6 +1,11 @@ import Swiper from "swiper"; -const AtomSliders = { +interface AtomSlidersStore { + init(): void; + initArticleSlider(): void; +} + +const AtomSliders: AtomSlidersStore = { init() { document.addEventListener("turbolinks:load", () => { this.initArticleSlider(); diff --git a/resources/themes/atom/js/components/ThemeSwitcher.js b/resources/themes/atom/js/components/ThemeSwitcher.ts similarity index 71% rename from resources/themes/atom/js/components/ThemeSwitcher.js rename to resources/themes/atom/js/components/ThemeSwitcher.ts index 21683b9..4b4acaa 100755 --- a/resources/themes/atom/js/components/ThemeSwitcher.js +++ b/resources/themes/atom/js/components/ThemeSwitcher.ts @@ -1,10 +1,19 @@ -const ThemeSwitcher = { +type Theme = "light" | "dark"; + +interface ThemeSwitcherStore { + currentTheme: Theme; + init(): void; + initButton(): void; + toggleTheme(): void; +} + +const ThemeSwitcher: ThemeSwitcherStore = { currentTheme: "light", init() { if ( localStorage.theme === "dark" || - (typeof window.matchMedia != "undefined" && + (typeof window.matchMedia !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches && localStorage.theme !== "light") ) { @@ -15,7 +24,7 @@ const ThemeSwitcher = { }, initButton() { - let themeSwitcher = document.getElementById("theme-switcher"); + const themeSwitcher = document.getElementById("theme-switcher"); themeSwitcher?.addEventListener("click", () => this.toggleTheme()); }, diff --git a/resources/themes/atom/views/client/nitro.blade.php b/resources/themes/atom/views/client/nitro.blade.php index bc3d232..5d2a66e 100755 --- a/resources/themes/atom/views/client/nitro.blade.php +++ b/resources/themes/atom/views/client/nitro.blade.php @@ -9,7 +9,7 @@ - @vite(['resources/themes/' . setting('theme') . '/css/app.css', 'resources/themes/' . setting('theme') . '/js/app.js'], 'build') + @vite(['resources/themes/' . setting('theme') . '/css/app.css', 'resources/themes/' . setting('theme') . '/js/app.ts'], 'build')