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.
-
-
-
-
- >
- )
-}
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.
+
+
+
+
+ >
+ );
+}
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 (
- <>
-
-
-
-
-
-
-
-
-
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/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')