devcontainer, proper CI/CD, etc etc
This commit is contained in:
40
.devcontainer/devcontainer.json
Normal file
40
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
{
|
||||||
|
"name": "Armandine",
|
||||||
|
"image": "mcr.microsoft.com/devcontainers/javascript-node:1-22-bookworm",
|
||||||
|
"features": {
|
||||||
|
"ghcr.io/devcontainers/features/docker-outside-of-docker:1": {}
|
||||||
|
},
|
||||||
|
"postCreateCommand": "corepack enable && corepack prepare pnpm@9.15.0 --activate && pnpm install",
|
||||||
|
"forwardPorts": [3000, 80],
|
||||||
|
"portsAttributes": {
|
||||||
|
"3000": {
|
||||||
|
"label": "Static site (pnpm serve)",
|
||||||
|
"onAutoForward": "notify"
|
||||||
|
},
|
||||||
|
"80": {
|
||||||
|
"label": "Nginx (when running container)",
|
||||||
|
"onAutoForward": "silent"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"customizations": {
|
||||||
|
"vscode": {
|
||||||
|
"extensions": [
|
||||||
|
"dbaeumer.vscode-eslint",
|
||||||
|
"esbenp.prettier-vscode",
|
||||||
|
"stylelint.vscode-stylelint"
|
||||||
|
],
|
||||||
|
"settings": {
|
||||||
|
"editor.formatOnSave": true,
|
||||||
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
|
"[css]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
||||||
|
"[javascript]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
||||||
|
"[html]": { "editor.defaultFormatter": "esbenp.prettier-vscode" },
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll.eslint": "explicit",
|
||||||
|
"source.fixAll.stylelint": "explicit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"remoteUser": "node"
|
||||||
|
}
|
||||||
35
.gitignore
vendored
Normal file
35
.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules
|
||||||
|
.pnpm-store
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Lockfiles from other package managers (we use pnpm)
|
||||||
|
package-lock.json
|
||||||
|
yarn.lock
|
||||||
|
bun.lockb
|
||||||
|
|
||||||
|
# Local env and secrets
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Logs and debug
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
|
||||||
|
# OS and IDE
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.idea
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# Build output (if added later)
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.next
|
||||||
|
out
|
||||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
6
.prettierrc
Normal file
6
.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"singleQuote": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"trailingComma": "es5"
|
||||||
|
}
|
||||||
1
.stylelintignore
Normal file
1
.stylelintignore
Normal file
@@ -0,0 +1 @@
|
|||||||
|
node_modules
|
||||||
4
.stylelintrc.json
Normal file
4
.stylelintrc.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": ["stylelint-config-recommended"],
|
||||||
|
"ignoreFiles": ["node_modules/**"]
|
||||||
|
}
|
||||||
129
.woodpecker/build.yaml
Normal file
129
.woodpecker/build.yaml
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
# Deploy: build image, push to registry, trigger Portainer stack redeploy.
|
||||||
|
# Runs on push/tag/manual to main only, after ci workflow succeeds.
|
||||||
|
when:
|
||||||
|
- branch: main
|
||||||
|
event: [push, tag, manual]
|
||||||
|
- event: deployment
|
||||||
|
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"'
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- ci
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Docker image build
|
||||||
|
image: docker:latest
|
||||||
|
environment:
|
||||||
|
REGISTRY_REPO: git.mifi.dev/mifi-holdings/armandine
|
||||||
|
DOCKER_API_VERSION: '1.43'
|
||||||
|
DOCKER_BUILDKIT: '1'
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- set -e
|
||||||
|
- echo "=== Building Docker image (BuildKit) ==="
|
||||||
|
- 'echo "Commit SHA: ${CI_COMMIT_SHA:0:8}"'
|
||||||
|
- 'echo "Registry repo: $REGISTRY_REPO"'
|
||||||
|
- |
|
||||||
|
build() {
|
||||||
|
docker build \
|
||||||
|
--progress=plain \
|
||||||
|
--tag $REGISTRY_REPO:${CI_COMMIT_SHA} \
|
||||||
|
--tag $REGISTRY_REPO:latest \
|
||||||
|
--label "git.commit=${CI_COMMIT_SHA}" \
|
||||||
|
--label "git.branch=${CI_COMMIT_BRANCH}" \
|
||||||
|
.
|
||||||
|
}
|
||||||
|
for attempt in 1 2 3; do
|
||||||
|
echo "Build attempt $attempt/3"
|
||||||
|
if build; then
|
||||||
|
echo "✓ Docker image built successfully"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
echo "Build attempt $attempt failed, retrying in 30s..."
|
||||||
|
sleep 30
|
||||||
|
done
|
||||||
|
echo "All build attempts failed"
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
- name: Send Docker Image Build Status Notification (success)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
DISCORD_WEBHOOK_URL:
|
||||||
|
from_secret: discord_webhook_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Docker image build success 🎉"}' "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||||
|
depends_on:
|
||||||
|
- Docker image build
|
||||||
|
when:
|
||||||
|
- status: [success]
|
||||||
|
|
||||||
|
- name: Send Docker Image Build Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
DISCORD_WEBHOOK_URL:
|
||||||
|
from_secret: discord_webhook_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Docker image build failure 💩"}' "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||||
|
depends_on:
|
||||||
|
- Docker image build
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
|
|
||||||
|
- name: Push to registry
|
||||||
|
image: docker:latest
|
||||||
|
environment:
|
||||||
|
DOCKER_API_VERSION: '1.43'
|
||||||
|
REGISTRY_URL: git.mifi.dev
|
||||||
|
REGISTRY_REPO: git.mifi.dev/mifi-holdings/armandine
|
||||||
|
REGISTRY_USERNAME:
|
||||||
|
from_secret: gitea_registry_username
|
||||||
|
REGISTRY_PASSWORD:
|
||||||
|
from_secret: gitea_package_token
|
||||||
|
volumes:
|
||||||
|
- /var/run/docker.sock:/var/run/docker.sock
|
||||||
|
commands:
|
||||||
|
- set -e
|
||||||
|
- echo "=== Pushing to registry ==="
|
||||||
|
- 'echo "Registry: $REGISTRY_URL"'
|
||||||
|
- 'echo "Repository: $REGISTRY_REPO"'
|
||||||
|
- |
|
||||||
|
echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" \
|
||||||
|
-u "$REGISTRY_USERNAME" \
|
||||||
|
--password-stdin
|
||||||
|
- docker push $REGISTRY_REPO:${CI_COMMIT_SHA}
|
||||||
|
- docker push $REGISTRY_REPO:latest
|
||||||
|
- echo "✓ Images pushed successfully"
|
||||||
|
depends_on:
|
||||||
|
- Docker image build
|
||||||
|
|
||||||
|
- name: Send Push to Registry Status Notification (success)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
DISCORD_WEBHOOK_URL:
|
||||||
|
from_secret: discord_webhook_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Push to registry success 🎉"}' "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||||
|
depends_on:
|
||||||
|
- Push to registry
|
||||||
|
when:
|
||||||
|
- status: [success]
|
||||||
|
|
||||||
|
- name: Send Push to Registry Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
DISCORD_WEBHOOK_URL:
|
||||||
|
from_secret: discord_webhook_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Push to registry failure 💩"}' "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||||
|
depends_on:
|
||||||
|
- Push to registry
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
33
.woodpecker/ci.yaml
Normal file
33
.woodpecker/ci.yaml
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
# CI: lint and format check. Runs on every PR and every push to main.
|
||||||
|
when:
|
||||||
|
- event: pull_request
|
||||||
|
- branch: main
|
||||||
|
event: push
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: install
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- corepack prepare pnpm@9.15.0 --activate
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: lint
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- corepack prepare pnpm@9.15.0 --activate
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
- pnpm lint
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
|
|
||||||
|
- name: format check
|
||||||
|
image: node:22-alpine
|
||||||
|
commands:
|
||||||
|
- corepack enable
|
||||||
|
- corepack prepare pnpm@9.15.0 --activate
|
||||||
|
- pnpm install --frozen-lockfile
|
||||||
|
- pnpm format:check
|
||||||
|
depends_on:
|
||||||
|
- install
|
||||||
59
.woodpecker/deploy.yaml
Normal file
59
.woodpecker/deploy.yaml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# Deploy: build image, push to registry, trigger Portainer stack redeploy.
|
||||||
|
# Runs on push/tag/manual to main only, after ci workflow succeeds.
|
||||||
|
when:
|
||||||
|
- branch: main
|
||||||
|
event: [push, tag, manual]
|
||||||
|
- event: deployment
|
||||||
|
evaluate: 'CI_PIPELINE_DEPLOY_TARGET == "production"'
|
||||||
|
|
||||||
|
depends_on:
|
||||||
|
- build
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Trigger Portainer stack redeploy
|
||||||
|
image: curlimages/curl:latest
|
||||||
|
environment:
|
||||||
|
PORTAINER_WEBHOOK_URL:
|
||||||
|
from_secret: portainer_webhook_url
|
||||||
|
commands:
|
||||||
|
- set -e
|
||||||
|
- echo "=== Triggering Portainer stack redeploy ==="
|
||||||
|
- |
|
||||||
|
resp=$(curl -s -w "\n%{http_code}" -X POST "$PORTAINER_WEBHOOK_URL")
|
||||||
|
body=$(echo "$resp" | head -n -1)
|
||||||
|
code=$(echo "$resp" | tail -n 1)
|
||||||
|
if [ "$code" != "200" ] && [ "$code" != "204" ]; then
|
||||||
|
echo "Webhook failed (HTTP $code): $body"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "✓ Portainer redeploy triggered (HTTP $code)"
|
||||||
|
depends_on:
|
||||||
|
- Push to registry
|
||||||
|
|
||||||
|
- name: Send Deploy Status Notification (success)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
DISCORD_WEBHOOK_URL:
|
||||||
|
from_secret: discord_webhook_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Production Deploy success 🎉"}' "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||||
|
depends_on:
|
||||||
|
- Trigger Portainer stack redeploy
|
||||||
|
when:
|
||||||
|
- status: [success]
|
||||||
|
|
||||||
|
- name: Send Deploy Status Notification (failure)
|
||||||
|
image: curlimages/curl
|
||||||
|
environment:
|
||||||
|
DISCORD_WEBHOOK_URL:
|
||||||
|
from_secret: discord_webhook_url
|
||||||
|
commands:
|
||||||
|
- |
|
||||||
|
BODY=$(printf '{"username":"WoodpeckerBot","content":"[%s - Build #%s] Production Deploy failure 💩"}' "$CI_REPO" "$CI_PIPELINE_NUMBER")
|
||||||
|
curl -sS -X POST -H "Content-Type: application/json" -d "$BODY" "$DISCORD_WEBHOOK_URL"
|
||||||
|
depends_on:
|
||||||
|
- Trigger Portainer stack redeploy
|
||||||
|
when:
|
||||||
|
- status: [failure]
|
||||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Armandine gallery – static site served by Nginx
|
||||||
|
FROM nginx:alpine
|
||||||
|
|
||||||
|
# Copy static site into default Nginx docroot
|
||||||
|
COPY src/ /usr/share/nginx/html/
|
||||||
|
|
||||||
|
# Optional: custom nginx config could be COPY'd here
|
||||||
|
# COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
EXPOSE 80
|
||||||
112
README.md
112
README.md
@@ -0,0 +1,112 @@
|
|||||||
|
# Armandine
|
||||||
|
|
||||||
|
Static gallery site served by Nginx. Runs as a repository-based container; image is built and pushed via Woodpecker CI/CD from this repo.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Site:** Static HTML/CSS/JS in `src/`, copied into the image.
|
||||||
|
- **Runtime:** `nginx:alpine` (see `Dockerfile`).
|
||||||
|
- **Registry:** Gitea at `git.mifi.dev` → image `git.mifi.dev/mifi-holdings/armandine`.
|
||||||
|
- **Deploy:** Portainer on Linode; stack uses this image (no volume for site content).
|
||||||
|
|
||||||
|
## Local development
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install dev dependencies (ESLint, Prettier, Stylelint)
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# Lint JS and CSS
|
||||||
|
pnpm lint
|
||||||
|
|
||||||
|
# Check formatting (CI uses this)
|
||||||
|
pnpm format:check
|
||||||
|
|
||||||
|
# Fix formatting
|
||||||
|
pnpm format
|
||||||
|
|
||||||
|
# Preview site locally (serves src/ on http://localhost:3000)
|
||||||
|
pnpm serve
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dev container
|
||||||
|
|
||||||
|
Open the repo in a dev container (VS Code/Cursor: **Dev Containers: Reopen in Container**) for a consistent environment:
|
||||||
|
|
||||||
|
- **Node 22** (matches CI), with `pnpm install` run after create
|
||||||
|
- **Docker (outside of Docker)** – uses the host Docker socket so you can run `pnpm build` and test the image inside the dev container
|
||||||
|
- **ESLint, Prettier, Stylelint** extensions plus format-on-save and fix-on-save
|
||||||
|
|
||||||
|
Port **3000** is forwarded for `pnpm serve`; port **80** is forwarded if you run the built Nginx container locally.
|
||||||
|
|
||||||
|
## Manual build and push
|
||||||
|
|
||||||
|
When you want to build and push the image yourself (e.g. before CI was set up, or for a one-off deploy):
|
||||||
|
|
||||||
|
1. Log in to the Gitea container registry:
|
||||||
|
```bash
|
||||||
|
docker login git.mifi.dev
|
||||||
|
```
|
||||||
|
Use your Gitea username and a token with package permissions.
|
||||||
|
|
||||||
|
2. Build the image (tags as `latest`):
|
||||||
|
```bash
|
||||||
|
pnpm build
|
||||||
|
```
|
||||||
|
This runs: `docker build -t git.mifi.dev/mifi-holdings/armandine:latest .`
|
||||||
|
|
||||||
|
3. Push to the registry:
|
||||||
|
```bash
|
||||||
|
pnpm push
|
||||||
|
```
|
||||||
|
Then on the server, redeploy the stack (e.g. Portainer “Pull and redeploy” or your webhook).
|
||||||
|
|
||||||
|
## Woodpecker CI/CD
|
||||||
|
|
||||||
|
Three pipelines (see `.woodpecker/`):
|
||||||
|
|
||||||
|
| Pipeline | When | What |
|
||||||
|
|----------|------|------|
|
||||||
|
| **ci** | Every push to `main`, every PR | Lint (ESLint + Stylelint) and Prettier check |
|
||||||
|
| **build**| Push/tag/manual on `main` only (after ci) | Build Docker image, push to `git.mifi.dev/mifi-holdings/armandine` |
|
||||||
|
| **deploy** | After build | Trigger Portainer stack redeploy via webhook |
|
||||||
|
|
||||||
|
Order: **ci** → **build** → **deploy**.
|
||||||
|
|
||||||
|
### Secrets (Woodpecker)
|
||||||
|
|
||||||
|
Configure in the repo’s Woodpecker secrets:
|
||||||
|
|
||||||
|
- `gitea_registry_username` – Gitea user for registry login
|
||||||
|
- `gitea_package_token` – Gitea token with package read/write
|
||||||
|
- `portainer_webhook_url` – Portainer stack webhook URL for redeploy
|
||||||
|
- `discord_webhook_url` – (optional) Discord notifications for build/deploy status
|
||||||
|
|
||||||
|
## Server / Portainer
|
||||||
|
|
||||||
|
- Stack is defined by `docker-compose.yml` in this repo.
|
||||||
|
- Compose uses the image from the registry (`git.mifi.dev/mifi-holdings/armandine:latest`); no volume for site content (it’s inside the image).
|
||||||
|
- Ensure the server can pull from `git.mifi.dev` (login or registry access). After a push, either let the deploy pipeline trigger the Portainer webhook or manually “Pull and redeploy” the stack.
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
├── src/ # Static site (copied into image)
|
||||||
|
│ ├── index.html
|
||||||
|
│ └── assets/
|
||||||
|
│ ├── css/
|
||||||
|
│ ├── js/
|
||||||
|
│ └── media/
|
||||||
|
├── Dockerfile # nginx:alpine + COPY src → /usr/share/nginx/html
|
||||||
|
├── docker-compose.yml # Stack for Portainer (Traefik, healthcheck)
|
||||||
|
├── package.json # Scripts: build, push, lint, format, serve (pnpm)
|
||||||
|
├── .devcontainer/ # Dev container (Node 22, Docker, lint/format tools)
|
||||||
|
├── .woodpecker/
|
||||||
|
│ ├── ci.yaml # Lint + format check (PR + main)
|
||||||
|
│ ├── build.yaml # Build image, push to registry
|
||||||
|
│ └── deploy.yaml # Portainer webhook
|
||||||
|
└── README.md # This file
|
||||||
|
```
|
||||||
|
|
||||||
|
## Version
|
||||||
|
|
||||||
|
`package.json` version: **1.0.0** (bump when you want to track releases).
|
||||||
|
|||||||
24
docker-compose.yml
Normal file
24
docker-compose.yml
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
services:
|
||||||
|
armandine-gallery:
|
||||||
|
image: git.mifi.dev/mifi-holdings/armandine:latest
|
||||||
|
container_name: mifi-holdings-armandine
|
||||||
|
networks:
|
||||||
|
- marina-net
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.docker.network=marina-net"
|
||||||
|
- "traefik.http.routers.armandine-gallery.rule=Host(`armandine.mifi.holdings`)"
|
||||||
|
- "traefik.http.routers.armandine-gallery.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.armandine-gallery.middlewares=security-prison@file"
|
||||||
|
- "traefik.http.routers.armandine-gallery.tls=true"
|
||||||
|
- "traefik.http.routers.armandine-gallery.tls.certresolver=letsencrypt"
|
||||||
|
- "traefik.http.services.armandine-gallery.loadbalancer.server.port=80"
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "wget --spider -q http://localhost/ || exit 1"]
|
||||||
|
interval: 20s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 3
|
||||||
|
|
||||||
|
networks:
|
||||||
|
marina-net:
|
||||||
|
external: true
|
||||||
26
eslint.config.js
Normal file
26
eslint.config.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import js from '@eslint/js';
|
||||||
|
import prettier from 'eslint-config-prettier';
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
files: ['src/**/*.js'],
|
||||||
|
...js.configs.recommended,
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 'latest',
|
||||||
|
sourceType: 'script',
|
||||||
|
globals: {
|
||||||
|
document: 'readonly',
|
||||||
|
window: 'readonly',
|
||||||
|
localStorage: 'readonly',
|
||||||
|
console: 'readonly',
|
||||||
|
fetch: 'readonly',
|
||||||
|
Image: 'readonly',
|
||||||
|
CustomEvent: 'readonly',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
rules: {
|
||||||
|
'no-unused-vars': ['warn', { argsIgnorePattern: '^_' }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
prettier,
|
||||||
|
];
|
||||||
26
package.json
Normal file
26
package.json
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"name": "armandine",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@9.15.0",
|
||||||
|
"description": "Armandine gallery – static Nginx site",
|
||||||
|
"scripts": {
|
||||||
|
"build": "docker build -t git.mifi.dev/mifi-holdings/armandine:latest .",
|
||||||
|
"push": "docker push git.mifi.dev/mifi-holdings/armandine:latest",
|
||||||
|
"lint": "pnpm lint:js && pnpm lint:css",
|
||||||
|
"lint:js": "eslint src",
|
||||||
|
"lint:css": "stylelint \"src/**/*.css\"",
|
||||||
|
"format": "prettier --write \"src/**/*.{html,css,js,json}\"",
|
||||||
|
"format:check": "prettier --check \"src/**/*.{html,css,js,json}\"",
|
||||||
|
"serve": "pnpm exec serve src -p 3000"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.15.0",
|
||||||
|
"eslint": "^9.15.0",
|
||||||
|
"eslint-config-prettier": "^9.1.0",
|
||||||
|
"prettier": "^3.3.3",
|
||||||
|
"stylelint": "^16.10.0",
|
||||||
|
"stylelint-config-recommended": "^14.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
1452
pnpm-lock.yaml
generated
Normal file
1452
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -46,10 +46,14 @@ body {
|
|||||||
padding: 1rem;
|
padding: 1rem;
|
||||||
}
|
}
|
||||||
@media (min-width: 768px) {
|
@media (min-width: 768px) {
|
||||||
.gallery-grid { column-count: 2; }
|
.gallery-grid {
|
||||||
|
column-count: 2;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@media (min-width: 1024px) {
|
@media (min-width: 1024px) {
|
||||||
.gallery-grid { column-count: 3; }
|
.gallery-grid {
|
||||||
|
column-count: 3;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.gallery-item {
|
.gallery-item {
|
||||||
margin: 0 0 1rem;
|
margin: 0 0 1rem;
|
||||||
@@ -79,7 +83,10 @@ body {
|
|||||||
}
|
}
|
||||||
#lightbox {
|
#lightbox {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top:0; left:0; right:0; bottom:0;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
background: rgba(0, 0, 0, 0.9);
|
background: rgba(0, 0, 0, 0.9);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -89,7 +96,7 @@ body {
|
|||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
}
|
}
|
||||||
#lightbox[aria-hidden="false"] {
|
#lightbox[aria-hidden='false'] {
|
||||||
visibility: visible;
|
visibility: visible;
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,8 +15,7 @@ const body = document.body;
|
|||||||
const lb = document.getElementById('lightbox');
|
const lb = document.getElementById('lightbox');
|
||||||
const lbCnt = document.getElementById('lb-content');
|
const lbCnt = document.getElementById('lb-content');
|
||||||
const lbCap = document.getElementById('lb-caption');
|
const lbCap = document.getElementById('lb-caption');
|
||||||
document.getElementById('lb-close')
|
document.getElementById('lb-close').addEventListener('click', () => {
|
||||||
.addEventListener('click', ()=>{
|
|
||||||
lb.setAttribute('aria-hidden', 'true');
|
lb.setAttribute('aria-hidden', 'true');
|
||||||
body.classList.remove('lightbox-open');
|
body.classList.remove('lightbox-open');
|
||||||
lbCnt.innerHTML = '';
|
lbCnt.innerHTML = '';
|
||||||
@@ -28,7 +27,7 @@ const mediaData= JSON.parse(document.getElementById('media-data').textContent);
|
|||||||
|
|
||||||
const createPicture = (item) => {
|
const createPicture = (item) => {
|
||||||
const pic = document.createElement('picture');
|
const pic = document.createElement('picture');
|
||||||
['desktop','tablet','mobile'].forEach(bp=>{
|
['desktop', 'tablet', 'mobile'].forEach((bp) => {
|
||||||
const src = document.createElement('source');
|
const src = document.createElement('source');
|
||||||
const widthQuery = bp === 'desktop' ? 1024 : bp === 'tablet' ? 768 : 0;
|
const widthQuery = bp === 'desktop' ? 1024 : bp === 'tablet' ? 768 : 0;
|
||||||
src.media = `(min-width:${widthQuery}px)`;
|
src.media = `(min-width:${widthQuery}px)`;
|
||||||
@@ -53,7 +52,7 @@ const createPicture = (item) => {
|
|||||||
return pic;
|
return pic;
|
||||||
};
|
};
|
||||||
|
|
||||||
mediaData.forEach(item=>{
|
mediaData.forEach((item) => {
|
||||||
const fig = document.createElement('figure');
|
const fig = document.createElement('figure');
|
||||||
fig.className = `gallery-item${item.type === 'video' ? ' video' : ''}`;
|
fig.className = `gallery-item${item.type === 'video' ? ' video' : ''}`;
|
||||||
fig.tabIndex = 0;
|
fig.tabIndex = 0;
|
||||||
@@ -69,7 +68,10 @@ mediaData.forEach(item=>{
|
|||||||
|
|
||||||
// events
|
// events
|
||||||
fig.addEventListener('click', () => openLightbox(item));
|
fig.addEventListener('click', () => openLightbox(item));
|
||||||
fig.addEventListener('keypress', e=> e.key==='Enter' && openLightbox(item));
|
fig.addEventListener(
|
||||||
|
'keypress',
|
||||||
|
(e) => e.key === 'Enter' && openLightbox(item)
|
||||||
|
);
|
||||||
|
|
||||||
gallery.appendChild(fig);
|
gallery.appendChild(fig);
|
||||||
});
|
});
|
||||||
@@ -77,7 +79,7 @@ mediaData.forEach(item=>{
|
|||||||
// --- video toggle ---
|
// --- video toggle ---
|
||||||
const videoTgl = document.getElementById('show_video');
|
const videoTgl = document.getElementById('show_video');
|
||||||
videoTgl.addEventListener('click', () => {
|
videoTgl.addEventListener('click', () => {
|
||||||
openLightbox(mediaData.find(i=>i.type==='video'));
|
openLightbox(mediaData.find((i) => i.type === 'video'));
|
||||||
});
|
});
|
||||||
|
|
||||||
function openLightbox(item) {
|
function openLightbox(item) {
|
||||||
|
|||||||
@@ -1,20 +1,42 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||||
<title>64 Armandine St #3 Boston, Massachusetts</title>
|
<title>64 Armandine St #3 Boston, Massachusetts</title>
|
||||||
<link rel="icon" type="image/png" sizes="32x32" href="assets/favicon-32x32.png">
|
<link
|
||||||
<link rel="icon" type="image/png" sizes="16x16" href="assets/favicon-16x16.png">
|
rel="icon"
|
||||||
<link rel="icon" type="image/x-icon" href="assets/favicon.ico">
|
type="image/png"
|
||||||
<link rel="stylesheet" href="assets/css/style.css">
|
sizes="32x32"
|
||||||
|
href="assets/favicon-32x32.png"
|
||||||
|
/>
|
||||||
|
<link
|
||||||
|
rel="icon"
|
||||||
|
type="image/png"
|
||||||
|
sizes="16x16"
|
||||||
|
href="assets/favicon-16x16.png"
|
||||||
|
/>
|
||||||
|
<link rel="icon" type="image/x-icon" href="assets/favicon.ico" />
|
||||||
|
<link rel="stylesheet" href="assets/css/style.css" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="site-header">
|
<header class="site-header">
|
||||||
<h1>64 Armandine St #3 Boston, Massachusetts</h1>
|
<h1>64 Armandine St #3 Boston, Massachusetts</h1>
|
||||||
<div class="buttons">
|
<div class="buttons">
|
||||||
<button id="show_video" class="emoji-button" aria-label="Show video tour">🎥</button>
|
<button
|
||||||
<button id="theme-toggle" class="emoji-button" aria-label="Toggle light/dark theme">🌓</button>
|
id="show_video"
|
||||||
|
class="emoji-button"
|
||||||
|
aria-label="Show video tour"
|
||||||
|
>
|
||||||
|
🎥
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
id="theme-toggle"
|
||||||
|
class="emoji-button"
|
||||||
|
aria-label="Toggle light/dark theme"
|
||||||
|
>
|
||||||
|
🌓
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user