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
This commit is contained in:
root
2026-06-18 17:00:00 +02:00
parent 4aa6f01779
commit e6d92f27b3
41 changed files with 986 additions and 680 deletions
-40
View File
@@ -1,40 +0,0 @@
import "./bootstrap";
import "./external/flowbite";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import Alpine from "alpinejs";
import Focus from "@alpinejs/focus";
import ArticleReactions from "./components/ArticleReactions.js";
import Swiper from "swiper";
import { Navigation, Pagination } from "swiper/modules";
ArticleReactions.init();
Alpine.plugin(Focus);
Alpine.start();
Swiper.use([Navigation, Pagination]);
// Swiper Initialization
document.addEventListener("DOMContentLoaded", function () {
const swiper = new Swiper(".swiper", {
// Your Swiper options here
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
pagination: {
el: ".swiper-pagination",
},
});
});
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;",
"",
);
+36
View File
@@ -0,0 +1,36 @@
import "./bootstrap";
import "./external/flowbite";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import Alpine from "alpinejs";
import Focus from "@alpinejs/focus";
import ArticleReactions from "./components/ArticleReactions";
import Swiper from "swiper";
import { Navigation, Pagination } from "swiper/modules";
ArticleReactions.init();
Alpine.plugin(Focus);
Alpine.start();
Swiper.use([Navigation, Pagination]);
document.addEventListener("DOMContentLoaded", function () {
const swiperEl = document.querySelector(".swiper");
if (swiperEl) {
new Swiper(swiperEl as HTMLElement, {
navigation: {
nextEl: ".swiper-button-next",
prevEl: ".swiper-button-prev",
},
pagination: {
el: ".swiper-pagination",
},
});
}
});
-31
View File
@@ -1,31 +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";
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_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'],
// });
+4
View File
@@ -0,0 +1,4 @@
import axios from "axios";
window.axios = axios;
window.axios.defaults.headers.common["X-Requested-With"] = "XMLHttpRequest";
@@ -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 };
+147
View File
@@ -0,0 +1,147 @@
import Alpine from "alpinejs";
interface ReactionData {
id: string;
name: string;
count: number;
users: string[];
}
interface ReactionComponent extends Record<string, unknown> {
url: string;
myReactions: string[];
articleReactions: ReactionData[];
allReactions: string[];
isAuthenticated: boolean;
init(): void;
treatArticleReactions(rawReactions: Record<string, { user?: { username: string } }[]>): 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, { user?: { username: string } }[]>, string];
const rawArticleReactions = args[1] as Record<string, { user?: { username: string } }[]> | 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<string, { user?: { username: string } }[]>) {
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 };
@@ -9,7 +9,7 @@
<link href="https://fonts.googleapis.com/css2?family=Ubuntu+Condensed&display=swap" rel="stylesheet">
@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')
@if(setting('button_enabled') == '1')
<style>
@@ -10,7 +10,7 @@
<!-- Fonts -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700&display=swap">
@vite(['resources/themes/' . setting('theme', 'dusk') . '/css/app.css', 'resources/themes/' . setting('theme') . '/js/app.js'], 'build')
@vite(['resources/themes/' . setting('theme', 'dusk') . '/css/app.css', 'resources/themes/' . setting('theme') . '/js/app.ts'], 'build')
<style>
:root {
@@ -11,7 +11,7 @@
<link rel="stylesheet" href="https://unpkg.com/flowbite@1.5.1/dist/flowbite.min.css"/>
<script src="https://unpkg.com/flowbite@1.5.1/dist/flowbite.js"></script>
@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')
</head>
<body class="h-screen overflow-hidden relative bg-[#233143]">
+4 -4
View File
@@ -12,10 +12,10 @@ export default defineConfig({
laravel({
input: [
path.resolve(__dirname, "css/app.css"),
path.resolve(__dirname, "js/app.js"),
"resources/js/global.js",
path.resolve(__dirname, "js/app.ts"),
"resources/js/global.ts",
"resources/css/global.css",
"resources/js/ssr.jsx",
"resources/js/ssr.tsx",
],
}),
@@ -33,7 +33,7 @@ export default defineConfig({
],
resolve: {
alias: {
"@": path.resolve(__dirname, "js/app.js"),
"@": path.resolve(__dirname, "js/app.ts"),
},
},
css: {