diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..e9a45b4
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,8 @@
+.git
+.github
+.forgejo
+dist
+node_modules
+npm-debug.log*
+Dockerfile*
+README.md
diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml
new file mode 100644
index 0000000..40a10b9
--- /dev/null
+++ b/.forgejo/workflows/ci.yml
@@ -0,0 +1,41 @@
+name: CI
+
+on:
+ push:
+ branches:
+ - main
+ pull_request:
+ branches:
+ - main
+ workflow_dispatch:
+
+permissions:
+ contents: read
+
+concurrency:
+ group: ci-${{ forgejo.ref }}
+ cancel-in-progress: true
+
+jobs:
+ verify:
+ runs-on: ubuntu-latest
+ timeout-minutes: 15
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 22
+ cache: npm
+
+ - name: Install dependencies
+ run: npm ci
+
+ - name: Lint
+ run: npm run lint
+
+ - name: Build
+ run: npm run build
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..ff67f4c
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,19 @@
+FROM node:22-alpine AS build
+
+WORKDIR /app
+
+COPY package.json package-lock.json ./
+RUN npm ci
+
+COPY . .
+RUN npm run build
+
+FROM nginx:1.27-alpine AS runtime
+
+COPY nginx.conf /etc/nginx/conf.d/default.conf
+COPY --from=build /app/dist /usr/share/nginx/html
+
+EXPOSE 80
+
+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
+ CMD wget -q --spider http://127.0.0.1/ || exit 1
diff --git a/README.md b/README.md
index e0eb78a..672b6b0 100644
--- a/README.md
+++ b/README.md
@@ -64,6 +64,12 @@ This project is built with:
Simply open [Lovable](https://lovable.dev/projects/5ee9d1e7-af25-47e9-ab4c-bdbca4554e32) and click on Share -> Publish.
+### Forgejo and Dokploy
+
+Pushes and pull requests targeting `main` are verified by the Forgejo Actions workflow in `.forgejo/workflows/ci.yml`. The production container is built from `Dockerfile` and serves the Vite output with NGINX, including React Router fallback handling.
+
+Dokploy is connected to the `Websites/interactives` Forgejo repository with autodeploy enabled. A successful push to `main` triggers a fresh Dockerfile build and replaces the running service only after the image has built successfully.
+
## Can I connect a custom domain to my Lovable project?
Yes, you can!
diff --git a/nginx.conf b/nginx.conf
new file mode 100644
index 0000000..aca9d71
--- /dev/null
+++ b/nginx.conf
@@ -0,0 +1,22 @@
+server {
+ listen 80;
+ listen [::]:80;
+ server_name _;
+
+ root /usr/share/nginx/html;
+ index index.html;
+
+ location / {
+ try_files $uri $uri/ /index.html;
+ }
+
+ location = /index.html {
+ add_header Cache-Control "no-cache";
+ }
+
+ location ~* \.(?:css|js|mjs|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2)$ {
+ expires 1y;
+ add_header Cache-Control "public, immutable";
+ try_files $uri =404;
+ }
+}
diff --git a/src/components/InteractiveGallery.tsx b/src/components/InteractiveGallery.tsx
index 1086712..24a3a03 100644
--- a/src/components/InteractiveGallery.tsx
+++ b/src/components/InteractiveGallery.tsx
@@ -67,7 +67,7 @@ const InteractiveGallery = () => {
Data Structures and Algorithms
-
Interactive explorations of elementary data structures and algorithms.
+
Interactive explorations of elementary data structures and algorithms.
{/* Search bar */}
@@ -103,4 +103,4 @@ const InteractiveGallery = () => {
);
};
-export default InteractiveGallery;
\ No newline at end of file
+export default InteractiveGallery;
diff --git a/src/components/InteractiveIndex.tsx b/src/components/InteractiveIndex.tsx
index 3017d22..4223abe 100644
--- a/src/components/InteractiveIndex.tsx
+++ b/src/components/InteractiveIndex.tsx
@@ -323,7 +323,7 @@ const InteractiveIndex = () => {
// Filter and sort interactives
const filteredAndSortedInteractives = useMemo(() => {
- let filtered = allInteractives.filter(interactive => {
+ const filtered = allInteractives.filter(interactive => {
const matchesSearch = interactive.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
interactive.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
interactive.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));
@@ -549,4 +549,4 @@ const InteractiveIndex = () => {
);
};
-export default InteractiveIndex;
\ No newline at end of file
+export default InteractiveIndex;
diff --git a/src/components/LadybugClockPuzzle.tsx b/src/components/LadybugClockPuzzle.tsx
index 4ad2044..7de133f 100644
--- a/src/components/LadybugClockPuzzle.tsx
+++ b/src/components/LadybugClockPuzzle.tsx
@@ -144,7 +144,7 @@ const LadybugClockPuzzle: React.FC
= ({
// Run to completion
const runToCompletion = useCallback(() => {
setIsPlaying(false);
- let currentSim = simulation;
+ const currentSim = simulation;
let position = currentSim.position;
const visited = new Set(currentSim.visited);
let moveCount = currentSim.moveCount;
@@ -545,4 +545,4 @@ const LadybugClockPuzzle: React.FC = ({
{showSocialShare && }
;
};
-export default LadybugClockPuzzle;
\ No newline at end of file
+export default LadybugClockPuzzle;
diff --git a/src/components/ZeckendorfGame.tsx b/src/components/ZeckendorfGame.tsx
index 439b0d0..1655e46 100644
--- a/src/components/ZeckendorfGame.tsx
+++ b/src/components/ZeckendorfGame.tsx
@@ -34,7 +34,7 @@ const ZeckendorfGame: React.FC = ({ showSocialShare = true
// Generate a random combination of Fibonacci numbers
const numTerms = Math.floor(Math.random() * 4) + 2; // 2-5 terms
let target = 0;
- let usedIndices = new Set();
+ const usedIndices = new Set();
for (let i = 0; i < numTerms; i++) {
let index;
@@ -235,4 +235,4 @@ const ZeckendorfGame: React.FC = ({ showSocialShare = true
);
};
-export default ZeckendorfGame;
\ No newline at end of file
+export default ZeckendorfGame;
diff --git a/src/components/guessing-game/QuestionGuessingMode.tsx b/src/components/guessing-game/QuestionGuessingMode.tsx
index d30855a..d76a11a 100644
--- a/src/components/guessing-game/QuestionGuessingMode.tsx
+++ b/src/components/guessing-game/QuestionGuessingMode.tsx
@@ -217,7 +217,7 @@ const QuestionGuessingMode: React.FC = ({
case 'odd':
baseCondition = (num % 2 !== 0);
break;
- case 'prime':
+ case 'prime': {
const isPrimeNum = (n: number): boolean => {
if (n < 2) return false;
for (let i = 2; i <= Math.sqrt(n); i++) {
@@ -227,6 +227,7 @@ const QuestionGuessingMode: React.FC = ({
};
baseCondition = isPrimeNum(num);
break;
+ }
case 'perfectSquare':
baseCondition = Math.sqrt(num) % 1 === 0;
break;
diff --git a/src/components/ui/command.tsx b/src/components/ui/command.tsx
index 56a0979..69d63a4 100644
--- a/src/components/ui/command.tsx
+++ b/src/components/ui/command.tsx
@@ -21,7 +21,7 @@ const Command = React.forwardRef<
))
Command.displayName = CommandPrimitive.displayName
-interface CommandDialogProps extends DialogProps {}
+type CommandDialogProps = DialogProps
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return (
diff --git a/src/components/ui/textarea.tsx b/src/components/ui/textarea.tsx
index 9f9a6dc..12c9136 100644
--- a/src/components/ui/textarea.tsx
+++ b/src/components/ui/textarea.tsx
@@ -2,8 +2,7 @@ import * as React from "react"
import { cn } from "@/lib/utils"
-export interface TextareaProps
- extends React.TextareaHTMLAttributes {}
+export type TextareaProps = React.TextareaHTMLAttributes
const Textarea = React.forwardRef(
({ className, ...props }, ref) => {
diff --git a/tailwind.config.ts b/tailwind.config.ts
index a4b00a3..a934352 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -1,4 +1,5 @@
import type { Config } from "tailwindcss";
+import tailwindcssAnimate from "tailwindcss-animate";
export default {
darkMode: ["class"],
@@ -122,5 +123,5 @@ export default {
}
}
},
- plugins: [require("tailwindcss-animate")],
+ plugins: [tailwindcssAnimate],
} satisfies Config;