Add Forgejo CI and Dokploy deployment
Some checks are pending
CI / verify (push) Waiting to run

This commit is contained in:
Neeldhara Misra 2026-07-20 14:22:11 +05:30
parent 263c87d6c7
commit 3d31dc1b55
13 changed files with 110 additions and 13 deletions

8
.dockerignore Normal file
View file

@ -0,0 +1,8 @@
.git
.github
.forgejo
dist
node_modules
npm-debug.log*
Dockerfile*
README.md

41
.forgejo/workflows/ci.yml Normal file
View file

@ -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

19
Dockerfile Normal file
View file

@ -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

View file

@ -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. 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? ## Can I connect a custom domain to my Lovable project?
Yes, you can! Yes, you can!

22
nginx.conf Normal file
View file

@ -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;
}
}

View file

@ -67,7 +67,7 @@ const InteractiveGallery = () => {
<div className="space-y-6"> <div className="space-y-6">
<div className="space-y-4"> <div className="space-y-4">
<h1 className="text-3xl font-bold text-foreground">Data Structures and Algorithms</h1> <h1 className="text-3xl font-bold text-foreground">Data Structures and Algorithms</h1>
<p className="text-muted-foreground text-lg">Interactive explorations of elementary data structures and algorithms.</p> <p className="text-muted-foreground text-lg">Interactive explorations of elementary data structures and algorithms.</p>
</div> </div>
{/* Search bar */} {/* Search bar */}

View file

@ -323,7 +323,7 @@ const InteractiveIndex = () => {
// Filter and sort interactives // Filter and sort interactives
const filteredAndSortedInteractives = useMemo(() => { const filteredAndSortedInteractives = useMemo(() => {
let filtered = allInteractives.filter(interactive => { const filtered = allInteractives.filter(interactive => {
const matchesSearch = interactive.title.toLowerCase().includes(searchTerm.toLowerCase()) || const matchesSearch = interactive.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
interactive.description.toLowerCase().includes(searchTerm.toLowerCase()) || interactive.description.toLowerCase().includes(searchTerm.toLowerCase()) ||
interactive.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase())); interactive.tags.some(tag => tag.toLowerCase().includes(searchTerm.toLowerCase()));

View file

@ -144,7 +144,7 @@ const LadybugClockPuzzle: React.FC<LadybugClockPuzzleProps> = ({
// Run to completion // Run to completion
const runToCompletion = useCallback(() => { const runToCompletion = useCallback(() => {
setIsPlaying(false); setIsPlaying(false);
let currentSim = simulation; const currentSim = simulation;
let position = currentSim.position; let position = currentSim.position;
const visited = new Set(currentSim.visited); const visited = new Set(currentSim.visited);
let moveCount = currentSim.moveCount; let moveCount = currentSim.moveCount;

View file

@ -34,7 +34,7 @@ const ZeckendorfGame: React.FC<ZeckendorfGameProps> = ({ showSocialShare = true
// Generate a random combination of Fibonacci numbers // Generate a random combination of Fibonacci numbers
const numTerms = Math.floor(Math.random() * 4) + 2; // 2-5 terms const numTerms = Math.floor(Math.random() * 4) + 2; // 2-5 terms
let target = 0; let target = 0;
let usedIndices = new Set<number>(); const usedIndices = new Set<number>();
for (let i = 0; i < numTerms; i++) { for (let i = 0; i < numTerms; i++) {
let index; let index;

View file

@ -217,7 +217,7 @@ const QuestionGuessingMode: React.FC<QuestionGuessingModeProps> = ({
case 'odd': case 'odd':
baseCondition = (num % 2 !== 0); baseCondition = (num % 2 !== 0);
break; break;
case 'prime': case 'prime': {
const isPrimeNum = (n: number): boolean => { const isPrimeNum = (n: number): boolean => {
if (n < 2) return false; if (n < 2) return false;
for (let i = 2; i <= Math.sqrt(n); i++) { for (let i = 2; i <= Math.sqrt(n); i++) {
@ -227,6 +227,7 @@ const QuestionGuessingMode: React.FC<QuestionGuessingModeProps> = ({
}; };
baseCondition = isPrimeNum(num); baseCondition = isPrimeNum(num);
break; break;
}
case 'perfectSquare': case 'perfectSquare':
baseCondition = Math.sqrt(num) % 1 === 0; baseCondition = Math.sqrt(num) % 1 === 0;
break; break;

View file

@ -21,7 +21,7 @@ const Command = React.forwardRef<
)) ))
Command.displayName = CommandPrimitive.displayName Command.displayName = CommandPrimitive.displayName
interface CommandDialogProps extends DialogProps {} type CommandDialogProps = DialogProps
const CommandDialog = ({ children, ...props }: CommandDialogProps) => { const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
return ( return (

View file

@ -2,8 +2,7 @@ import * as React from "react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
export interface TextareaProps export type TextareaProps = React.TextareaHTMLAttributes<HTMLTextAreaElement>
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>( const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => { ({ className, ...props }, ref) => {

View file

@ -1,4 +1,5 @@
import type { Config } from "tailwindcss"; import type { Config } from "tailwindcss";
import tailwindcssAnimate from "tailwindcss-animate";
export default { export default {
darkMode: ["class"], darkMode: ["class"],
@ -122,5 +123,5 @@ export default {
} }
} }
}, },
plugins: [require("tailwindcss-animate")], plugins: [tailwindcssAnimate],
} satisfies Config; } satisfies Config;