From d2de0d9db438aa8085773ab3ef188eed9bb79d14 Mon Sep 17 00:00:00 2001 From: "m::r" Date: Mon, 12 Jan 2026 10:48:54 +0000 Subject: [PATCH 1/4] chore(dev-env): config --- .env.local.example | 12 ++++++++ Dockerfile.local | 42 ++++++++++++++++++++++++++ Makefile | 64 ++++++++++++++++++++++++++++++++++++++++ docker-compose.local.yml | 23 +++++++++++++++ makefile | 32 -------------------- 5 files changed, 141 insertions(+), 32 deletions(-) create mode 100644 .env.local.example create mode 100644 Dockerfile.local create mode 100644 Makefile create mode 100644 docker-compose.local.yml delete mode 100644 makefile diff --git a/.env.local.example b/.env.local.example new file mode 100644 index 0000000..4f273d9 --- /dev/null +++ b/.env.local.example @@ -0,0 +1,12 @@ +# Local development environment variables +# Copy this file to .env.local and adjust values as needed + +# Flags API endpoint (flags-api service) +# Local dev: https://localhost:4430 +# Production: https://api.flags.izeebot.top +REACT_APP_API_URL=https://localhost:4430 + +# Auth service endpoint (auth/openid service) +# Local dev: https://localhost:8548 +# Production: https://auth.izeebot.top +REACT_APP_AUTH_URL=https://localhost:8548 diff --git a/Dockerfile.local b/Dockerfile.local new file mode 100644 index 0000000..ae33cc3 --- /dev/null +++ b/Dockerfile.local @@ -0,0 +1,42 @@ +# Local development Dockerfile with configurable API endpoints +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package.json yarn.lock* package-lock.json* ./ + +# Install dependencies +RUN if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + else npm install; fi + +# Copy source code +COPY . . + +# Build arguments for API endpoints +ARG REACT_APP_API_URL=https://localhost:4430 +ARG REACT_APP_AUTH_URL=https://localhost:8548 +ARG APP_VERSION="local" + +# Set environment variables for build +ENV NODE_OPTIONS=--openssl-legacy-provider +ENV REACT_APP_API_URL=${REACT_APP_API_URL} +ENV REACT_APP_AUTH_URL=${REACT_APP_AUTH_URL} +ENV REACT_APP_VERSION=${APP_VERSION} + +# Build the production bundle +RUN if [ -f yarn.lock ]; then yarn build; else npm run build; fi + +# Stage 2: Serve with Caddy +FROM caddy:2-alpine + +# Copy built files from builder stage +COPY --from=builder /app/build /srv + +# Copy Caddyfile +COPY Caddyfile /etc/caddy/Caddyfile + +EXPOSE 80 + +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2ad8dc3 --- /dev/null +++ b/Makefile @@ -0,0 +1,64 @@ +.PHONY: help build up down rebuild dev test install clean logs sh network + +CYAN := \033[0;36m +RESET := \033[0m + +# Local compose file +LOCAL_COMPOSE := docker compose -f docker-compose.local.yml + +# Default target +help: ## Show this help message + @printf "\\nUsage: make $(CYAN)[target]$(RESET)\\n\\n" + @echo 'Targets:' + @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(CYAN)%-22s$(RESET) %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort -f + +install: ## Install dependencies + @yarn install + +dev: ## Start development server (yarn start) + @echo "Starting dev server at http://localhost:3000" + @yarn start + +build: ## Build production bundle + @yarn build + +test: ## Run tests + @yarn test + +# Docker targets +docker-build: ## Build local Docker image + @$(LOCAL_COMPOSE) build + +docker-up: ## Start local Docker container + @$(LOCAL_COMPOSE) up -d + +docker-down: ## Stop local Docker container + @$(LOCAL_COMPOSE) down + +docker-rebuild: docker-down docker-build docker-up ## Rebuild and restart Docker container + +docker-logs: ## Show Docker container logs + @$(LOCAL_COMPOSE) logs -f + +docker-sh: ## Shell into Docker container + @$(LOCAL_COMPOSE) exec app sh + +# Full local setup +local: docker-build docker-up ## Build and start local Docker setup + @echo "App running at http://localhost:3001" + +# Network setup +network: ## Create required Docker networks + @docker network create backend-flags 2>/dev/null || true + @docker network create openid_network 2>/dev/null || true + @echo "Networks created (or already exist)" + +# Cleanup +clean: ## Clean build artifacts + @rm -rf build/ + @rm -rf node_modules/.cache/ + @echo "Cleaned build artifacts" + +clean-all: clean ## Clean all (including node_modules) + @rm -rf node_modules/ + @echo "Cleaned node_modules" diff --git a/docker-compose.local.yml b/docker-compose.local.yml new file mode 100644 index 0000000..451fd61 --- /dev/null +++ b/docker-compose.local.yml @@ -0,0 +1,23 @@ +services: + app: + build: + context: . + dockerfile: Dockerfile.local + args: + REACT_APP_API_URL: ${REACT_APP_API_URL:-https://localhost:4430} + REACT_APP_AUTH_URL: ${REACT_APP_AUTH_URL:-https://localhost:8548} + container_name: flagsapp-local + ports: + - "3001:80" + networks: + - backend-flags + - openid_network + environment: + - REACT_APP_API_URL=${REACT_APP_API_URL:-https://localhost:4430} + - REACT_APP_AUTH_URL=${REACT_APP_AUTH_URL:-https://localhost:8548} + +networks: + backend-flags: + external: true + openid_network: + external: true diff --git a/makefile b/makefile deleted file mode 100644 index e5c58a1..0000000 --- a/makefile +++ /dev/null @@ -1,32 +0,0 @@ -include .env -reset: -#Delete the relevant section from the .gitmodules file. - /bin/rm -rf .gitmodules - > .gitmodules -#Stage the .gitmodules changes git add .gitmodules - git add .gitmodules -#Delete the relevant section from .git/config.# ^(\[submodule)(.+(\n))+(?=\[) # sed -e "s/^(\[submodule)(.+(\\n))+(?=\[)/gm" - @echo "Delete the relevant section from .git/config" - @/bin/rm .git/config - git init -#Run git rm --cached path_to_submodule - git rm -r --cached ${CODE_PATH} || true -#Run rm -rf .git/modules/path_to_submodule - /bin/rm -rf .git/modules/${CODE_PATH} -#Commit git commit -m "Removed submodule" - git commit -am "Removed submodule" - git reset --hard HEAD~1 -#rm -rf path_to_submodule - /bin/rm -rf ${CODE_PATH} - git init - git remote add origin ${GIT_REMOTE_URL} - git pull origin master -init: init-submodule -init-submodule: - git submodule add -b ${BRANCH} -- ${GIT_URL} ${CODE_PATH} - git submodule update --init - cd ${CODE_PATH} && make init -run: - cd ${CODE_PATH} && docker-compose up -build-containers: - cd ${CODE_PATH} && docker-compose build \ No newline at end of file From c77189ca296bc8f405c7af7f730fb3d809ac106a Mon Sep 17 00:00:00 2001 From: "m::r" Date: Tue, 13 Jan 2026 01:32:42 +0000 Subject: [PATCH 2/4] WIP dev env v0.1 --- .env | 15 ++++++---- .env.development | 2 ++ .env.local.example | 15 +++------- Dockerfile.local | 20 ++++--------- Makefile | 4 ++- docker-compose.local.yml | 7 ++--- docker-compose.override.yml | 25 ++++++++++++++++ src/App.js | 2 +- src/config/Api.js | 4 +-- src/hooks/useOAuth.js | 60 ++++++++++++++++++------------------- 10 files changed, 82 insertions(+), 72 deletions(-) create mode 100644 .env.development create mode 100644 docker-compose.override.yml diff --git a/.env b/.env index 6dbd696..f08abdb 100644 --- a/.env +++ b/.env @@ -1,5 +1,10 @@ -PROJECT_NAME=api -GIT_URL=git@github.com:mainstreamer/docker-config.git -BRANCH='flags-api' -GIT_REMOTE_URL=git@github.com:mainstreamer/flagsapp.git -CODE_PATH=.docker +# Production environment (default) +#REACT_APP_API_URL=https://api.flags.izeebot.top +#REACT_APP_AUTH_URL=https://auth.izeebot.top +REACT_APP_API_URL=https://localhost:8000 + +# Auth service endpoint (auth/openid service) +# Local dev: https://localhost:8548 +# Production: https://auth.izeebot.top +#REACT_APP_AUTH_URL=https://localhost:8548 +REACT_APP_AUTH_URL=https://localhost:8547 \ No newline at end of file diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..6165a1f --- /dev/null +++ b/.env.development @@ -0,0 +1,2 @@ +# Local development environment +REACT_APP_API_URL=http://localhost:8000 diff --git a/.env.local.example b/.env.local.example index 4f273d9..9aed83d 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,12 +1,5 @@ -# Local development environment variables -# Copy this file to .env.local and adjust values as needed +# Local overrides (copy to .env.local) +# This file is gitignored -# Flags API endpoint (flags-api service) -# Local dev: https://localhost:4430 -# Production: https://api.flags.izeebot.top -REACT_APP_API_URL=https://localhost:4430 - -# Auth service endpoint (auth/openid service) -# Local dev: https://localhost:8548 -# Production: https://auth.izeebot.top -REACT_APP_AUTH_URL=https://localhost:8548 +# Flags API - local: http://localhost:8000, prod: https://api.flags.izeebot.top +REACT_APP_API_URL=http://localhost:8000 diff --git a/Dockerfile.local b/Dockerfile.local index ae33cc3..6882988 100644 --- a/Dockerfile.local +++ b/Dockerfile.local @@ -3,38 +3,28 @@ FROM node:18-alpine AS builder WORKDIR /app -# Copy package files COPY package.json yarn.lock* package-lock.json* ./ -# Install dependencies RUN if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \ elif [ -f package-lock.json ]; then npm ci; \ else npm install; fi -# Copy source code COPY . . -# Build arguments for API endpoints -ARG REACT_APP_API_URL=https://localhost:4430 -ARG REACT_APP_AUTH_URL=https://localhost:8548 -ARG APP_VERSION="local" +# Build arguments for API endpoints (HTTP for local dev) +ARG REACT_APP_API_URL=http://localhost:8000 +ARG REACT_APP_AUTH_URL=http://localhost:8547 -# Set environment variables for build ENV NODE_OPTIONS=--openssl-legacy-provider ENV REACT_APP_API_URL=${REACT_APP_API_URL} ENV REACT_APP_AUTH_URL=${REACT_APP_AUTH_URL} -ENV REACT_APP_VERSION=${APP_VERSION} -# Build the production bundle -RUN if [ -f yarn.lock ]; then yarn build; else npm run build; fi +RUN yarn build -# Stage 2: Serve with Caddy +# Serve with Caddy FROM caddy:2-alpine -# Copy built files from builder stage COPY --from=builder /app/build /srv - -# Copy Caddyfile COPY Caddyfile /etc/caddy/Caddyfile EXPOSE 80 diff --git a/Makefile b/Makefile index 2ad8dc3..6e93b07 100644 --- a/Makefile +++ b/Makefile @@ -15,8 +15,10 @@ help: ## Show this help message install: ## Install dependencies @yarn install -dev: ## Start development server (yarn start) +dev: ## Start development server at http://localhost:3000 @echo "Starting dev server at http://localhost:3000" + @echo "Using API: $${REACT_APP_API_URL:-http://localhost:8000}" + @echo "Using Auth: $${REACT_APP_AUTH_URL:-http://localhost:8547}" @yarn start build: ## Build production bundle diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 451fd61..de73558 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -4,17 +4,14 @@ services: context: . dockerfile: Dockerfile.local args: - REACT_APP_API_URL: ${REACT_APP_API_URL:-https://localhost:4430} - REACT_APP_AUTH_URL: ${REACT_APP_AUTH_URL:-https://localhost:8548} + REACT_APP_API_URL: ${REACT_APP_API_URL:-http://localhost:8000} + REACT_APP_AUTH_URL: ${REACT_APP_AUTH_URL:-http://localhost:8547} container_name: flagsapp-local ports: - "3001:80" networks: - backend-flags - openid_network - environment: - - REACT_APP_API_URL=${REACT_APP_API_URL:-https://localhost:4430} - - REACT_APP_AUTH_URL=${REACT_APP_AUTH_URL:-https://localhost:8548} networks: backend-flags: diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..2c9c258 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,25 @@ +services: + app: + env_file: + - .env.local + build: + context: . + dockerfile: Dockerfile.local + args: + REACT_APP_API_URL: ${REACT_APP_API_URL:-https://localhost:3001} + REACT_APP_AUTH_URL: ${REACT_APP_AUTH_URL:-https://localhost:8547} + container_name: flagsapp-local + ports: + - "3001:80" + networks: + - backend-flags + - openid_network + environment: + - REACT_APP_API_URL=${REACT_APP_API_URL:-https://localhost:3001} + - REACT_APP_AUTH_URL=${REACT_APP_AUTH_URL:-https://localhost:8547} + +networks: + backend-flags: + external: true + openid_network: + external: true diff --git a/src/App.js b/src/App.js index 6ec7022..7937817 100644 --- a/src/App.js +++ b/src/App.js @@ -52,7 +52,7 @@ function App() { - + {/**/} diff --git a/src/config/Api.js b/src/config/Api.js index 3f90815..aa88354 100644 --- a/src/config/Api.js +++ b/src/config/Api.js @@ -1,7 +1,5 @@ const api = { - 'url' : 'https://api.flags.izeebot.top', - // 'mode' : 'prod', - // 'mode' : 'dev' + url: process.env.REACT_APP_API_URL || 'http://localhost:8000', }; export default api; diff --git a/src/hooks/useOAuth.js b/src/hooks/useOAuth.js index 622b075..8c9ac3e 100644 --- a/src/hooks/useOAuth.js +++ b/src/hooks/useOAuth.js @@ -15,53 +15,51 @@ export const useOAuth = () => { const left = window.screen.width / 2 - width / 2; const top = window.screen.height / 2 - height / 2; + // Open popup to backend login endpoint + // Backend handles OAuth flow and token exchange securely const popup = window.open( - api.url + '/login?popup=1', + `${api.url}/login`, 'OAuth Login', `width=${width},height=${height},left=${left},top=${top}` ); const handleMessage = (event) => { - // Debug logging - console.log('OAuth message received:', event.origin, event.data); - - // Verify origin - if (event.origin !== api.url) { - console.warn('OAuth message from incorrect origin:', event.origin, 'expected:', api.url); + // Only handle oauth_success messages + if (!event.data || event.data.type !== 'oauth_success') { return; } - if (event.data.type === 'oauth_success') { - console.log('OAuth success! Storing tokens...'); + console.log('OAuth success received from backend'); - // Store tokens - localStorage.setItem('accessToken', event.data.access_token); - localStorage.setItem('refreshToken', event.data.refresh_token); + const { access_token, refresh_token, expires_in } = event.data; - // Store expiration time if provided - if (event.data.expires_in) { - const expiresAt = Date.now() + (event.data.expires_in * 1000); - localStorage.setItem('tokenExpiresAt', expiresAt.toString()); - } + // Store tokens + localStorage.setItem('accessToken', access_token); + if (refresh_token) { + localStorage.setItem('refreshToken', refresh_token); + } - // Set axios default header - axios.defaults.headers.common = { - 'Authorization': `Bearer ${event.data.access_token}` - }; + // Store expiration time if provided + if (expires_in) { + const expiresAt = Date.now() + (expires_in * 1000); + localStorage.setItem('tokenExpiresAt', expiresAt.toString()); + } - console.log('Token stored, redirecting to game...'); + // Set axios default header + axios.defaults.headers.common = { + 'Authorization': `Bearer ${access_token}` + }; - setIsLoading(false); - if (popup) popup.close(); + console.log('Token stored, redirecting to game...'); - // Clean up - window.removeEventListener('message', handleMessage); + setIsLoading(false); + if (popup) popup.close(); - // Redirect to flags page - history.push('/flagsapi'); - } else { - console.warn('Unexpected OAuth message type:', event.data.type); - } + // Clean up + window.removeEventListener('message', handleMessage); + + // Redirect to flags page + history.push('/flagsapi'); }; window.addEventListener('message', handleMessage); From c05321ee1dbc8c71f8664069a084a217982efa80 Mon Sep 17 00:00:00 2001 From: "m::r" Date: Tue, 13 Jan 2026 02:11:41 +0000 Subject: [PATCH 3/4] feat(popup): fixed critical errors, added logout, design fixes --- src/components/flags/FlagsApi.js | 10 +- src/components/flags/FlagsApi.js.save | 379 ++++++++++++++++++++++++ src/components/flags/FlagsApi.js.save.1 | 379 ++++++++++++++++++++++++ src/components/flags/Profile.js | 14 +- src/components/flags/styles.css | 23 +- src/components/home/Home.js | 56 +++- src/components/home/styles.css | 73 ++++- src/reducers/initial.js | 22 +- 8 files changed, 931 insertions(+), 25 deletions(-) create mode 100644 src/components/flags/FlagsApi.js.save create mode 100644 src/components/flags/FlagsApi.js.save.1 diff --git a/src/components/flags/FlagsApi.js b/src/components/flags/FlagsApi.js index b79bae9..9c9c7a7 100644 --- a/src/components/flags/FlagsApi.js +++ b/src/components/flags/FlagsApi.js @@ -267,7 +267,7 @@ class FlagsApi extends React.Component { {/**/} - + Question: @@ -313,9 +313,11 @@ class FlagsApi extends React.Component { 'margin' : '10px', 'margin-bottom' : '0px' }}> - - {this.props.text}  - +
+ + {this.props.text}  + +
Total time: {this.props.sessionTimer}
{ + this.showFlags(); + }, 1500); + + } else { + this.showCorrect(); + if (this.props.lifes == 1) { + this.props.dispatch({type : 'incorrect'}) + this.gameOver(); + } else { + this.stopTimer(); + this.props.dispatch({type : 'incorrect'}) + setTimeout(() => { + this.showFlags(); + }, 1500); + } + } + } + + incorrect() { + console.log('Incorrect'); + this.showCorrect(); + this.props.dispatch({type: 'incorrect'}) + if (this.props.lifes == 0) { + this.gameOver(); + } else { + setTimeout(() => { + this.showFlags(); + }, 1500); + } + } + + timeout() { + this.incorrect(); + this.stopTimer(); + } + + restartTimer() { + this.startTimer(); + } + + async startGame() { + this.gameEnded = false; + this.answerLocked = false; + this.setState({ loading: true }); + this.stopTimer(); + await this.handleClick('api') + .then(() => this.startTimer()) + .then(() => this.prepareStat()) + .then(() => this.setState({ loading: false })); + } + + async showFlags() { + await this.handleClick('api').then(() => + { + this.restartTimer(); + } + ).then(() => this.prepareStat()) + .then(() => { + this.answerLocked = false; + }); + } + + async gameOver() { + if (this.gameEnded) { + return; + } + this.gameEnded = true; + this.stopTimer(); + await this.submitScore(this.props.counter, this.props.sessionTimer); + } + + array = []; + + startTimer = () => { + console.log('Timer start'); + this.props.dispatch({type: 'restartTimer'}); + + let interval = setInterval(() => { + this.props.dispatch({type: 'tick'}); + this.tickTimer(); + }, 1000); + + this.stopTimer(); + this.array.push(interval); + } + + stopTimer() { + this.array.map(item => clearInterval(item)); + this.array = []; + console.log('Timer stop'); + } + + tickTimer() { + if (this.props.timer == 15) { + + } else { + if (this.props.timer == 0) { + this.timeout(); + + } + } + } + + showCorrect() { + console.log('Show correct'); + let element = document.getElementById('correct'); + ReactDOM.findDOMNode(element).style.border = '3px dotted #079430'; + setTimeout(() => { + this.hideCorrect(); + }, 1500); + } + + hideCorrect() { + console.log('Hide correct'); + let elements = document.getElementsByClassName('flag'); + for (let item of elements) { + ReactDOM.findDOMNode(item).style.border = '3px dotted transparent'; + } + } + + submitScore(score, sessionTimer) { + const res = axios.post(api.url+'/api/flags/scores', { 'score' : score, 'sessionTimer' : sessionTimer, 'answers' : this.answers }); + this.answers = []; + console.log(res); + } + + componentDidMount() { + window.innerWidth = 500; + this.startGame(); + // this.foo(); + } + componentWillUnmount() { + this.gameOver(); + } + + + restartGame() { + console.log('RESTART'); + this.props.dispatch({type : 'reset'}); + this.gameOver(); + this.startGame(); + } + + answers = []; + question = []; + prepareStat() { + // console.log('FOO'); + // console.log(this.props); + // console.log(this.props.flags); + this.props.flagi.map((item) => + // () => alert() + this.question.push(item) + // console.log(item) + // (item) => { alert(); console.log('xaxa' + item)} + ); + + + // this.array.map(item => clearInterval(item)); + + + // console.log(); + // for (let item of this.props.flagi) { + // this.question.push(item.getAllKeys()[0]) + // } + // console.log(this.question); + } + + saveAnswer(correct) { + let answer = { + correct : correct, + answerCode : this.props.answerCode, + options : this.question.filter((item) => item !== this.props.answerCode), + time : this.props.maxTimer - this.props.timer + } + + this.answers.push(answer); + this.question = []; + console.log(this.answers); + } + + render() { + if (this.state.loading) { + return
Fetching question...
; + } + + return ( +
+ {/*// */} + + + {/**/} + + + Question: + + +
+ Select the flag of + Time: {this.props.timer} + +
+ +

{this.props.ques}

+ + { + this.props.flags.map(item => + ( + this.answer(item)}> + { + this.props.answer == item + ? {item} + : {item} + } + + ) + ) + } + + + GAME OVER! Your score: {this.props.counter} + + +
+ + {this.props.text}  + + Total time: {this.props.sessionTimer} +
+
+ {this.props.lifesIcon} + Score: {this.props.counter} +
+ +
+
+ {/**/} + +
+
+   + +
+
+ ) +} + redirect = () => { + this.gameOver(); + this.props.dispatch({type : 'reset'}); + this.props.history.push('/'); + } + + exitGame = () => { + this.gameOver(); + this.props.dispatch({type : 'reset'}); + this.props.history.push('/'); + } +} + +function mapStateToProps (state) { + return { + counter: state.add.counter, + text : state.add.text, + flagi : state ? Object.keys(state.add.flags) : {}, + flags : state ? Object.values(state.add.flags) : {}, + ques : state.add.ques, + answer : state.add.answer, + answerCode : state.add.answerCode, + token : state.add.token, + lifes : state.add.lifes, + lifesIcon : state.add.lifesIcon, + timer: state.add.timer, + interval : state.add.interval, + maxTimer: state.add.maxTimer, + sessionTimer: state.add.sessionTimer, + } +} + +export default connect(mapStateToProps)(FlagsApi); diff --git a/src/components/flags/FlagsApi.js.save.1 b/src/components/flags/FlagsApi.js.save.1 new file mode 100644 index 0000000..26cf5db --- /dev/null +++ b/src/components/flags/FlagsApi.js.save.1 @@ -0,0 +1,379 @@ +OOOBimport React from 'react'; +iOOBmport { connect } from 'react-redux' +import TelegramLoginButton, { TelegramUser } from 'telegram-login-button' +impoBrt { Link } from 'react-router-dom'; +impOOOOOort { useDispatch, useStore } from 'react-redux' +import axios from "../../config/Axios"; +import "./sAOAAAOAtyles.css"; +imporOOOBt BAAAutton from 'react-bootstrap/Button'; +import Jumbotron from 'react-bootstrap/Jumbotron'; +import Toast from 'react-bootstrap/Toast'; +import Container from 'react-bootstrap/Container'; +import { Row, Col } from "react-bootstrap"; +import Card from "react-bootstrap/Card"; +import ReactDOM from 'react-dom'; +import Alert from 'react-bootstrap/Alert'; +import api f:rom "../../config/Api"; +import {fOOAorEach} from "react-bootstrap/ElementChildren"; +import { useHistory } from "react-router-dom"; + + +class FlagsApi extends React.Component { + + gamOAeEnded = false; +OOOOOOA answerLocked = falOse; + + stateOB = { + loaOBding: true + };O + + asyOBnc handleClick(action) { + if (action === 'api') { + const res = await axios.get(api.url+'/api/flags/test'); + BOBBBO this.props.dispatch( + BBBBOO {type : 'set', payload: + {O 'text' : res.data.message, + 'flags' : res.data.flags, + 'ques' : res.data.ques, + 'answer' : res.data.answer, + 'answerCode' : res.data.answerCode, + 'counter' : this.props.counter, + 'lifes' : this.props.lifes, + 'lifesIcon' : this.props.lifesIcon, + 'timer' : this.props.timer, + 'interval' : this.props.interval, + 'maxTimer' : this.props.maxTimer, + 'sessionTimer' : this.props.sessionTimer, + 'flagi' : res.data.flags, + } + } + ); + } + + if (action === 'increment') { + this.props.dispatch({type : 'add'}); + } + + if (action === 'protected') { + const res = await axios.get(api.url+'/api/flags/protected'); + } + } + + async answer(action) { + if (this.props.lifes == 0) { return; } + if (this.answerLocked) { return; } + + this.answerLocked = true; + + if (action === this.props.answer) { + this.saveAnswer(true); + } else { + this.saveAnswer(false); + } + + if (action === this.props.answer) { + this.stopTimer(); + await axios.post(api.url+'/api/flags/correct/'+this.props.answerCode); + this.props.dispatch({type : 'correct' }) + setTimeout(() => { + this.showFlags(); + }, 1500); + + } else { + this.showCorrect(); + if (this.props.lifes == 1) { + this.props.dispatch({type : 'incorrect'}) + this.gameOver(); + } else { + this.stopTimer(); + this.props.dispatch({type : 'incorrect'}) + setTimeout(() => { + this.showFlags(); + }, 1500); + } + } + } + + incorrect() { + console.log('Incorrect'); + this.showCorrect(); + this.props.dispatch({type: 'incorrect'}) + if (this.props.lifes == 0) { + this.gameOver(); + } else { + setTimeout(() => { + this.showFlags(); + }, 1500); + } + } + + timeout() { + this.incorrect(); + this.stopTimer(); + } + + restartTimer() { + this.startTimer(); + } + + async startGame() { + this.gameEnded = false; + this.answerLocked = false; + this.setState({ loading: true }); + this.stopTimer(); + await this.handleClick('api') + .then(() => this.startTimer()) + .then(() => this.prepareStat()) + .then(() => this.setState({ loading: false })); + } + + async showFlags() { + await this.handleClick('api').then(() => + { + this.restartTimer(); + } + ).then(() => this.prepareStat()) + .then(() => { + this.answerLocked = false; + }); + } + + async gameOver() { + if (this.gameEnded) { + return; + } + this.gameEnded = true; + this.stopTimer(); + await this.submitScore(this.props.counter, this.props.sessionTimer); + } + + array = []; + + startTimer = () => { + console.log('Timer start'); + this.props.dispatch({type: 'restartTimer'}); + + let interval = setInterval(() => { + this.props.dispatch({type: 'tick'}); + this.tickTimer(); + }, 1000); + + this.stopTimer(); + this.array.push(interval); + } + + stopTimer() { + this.array.map(item => clearInterval(item)); + this.array = []; + console.log('Timer stop'); + } + + tickTimer() { + if (this.props.timer == 15) { + + } else { + if (this.props.timer == 0) { + this.timeout(); + + } + } + } + + showCorrect() { + console.log('Show correct'); + let element = document.getElementById('correct'); + ReactDOM.findDOMNode(element).style.border = '3px dotted #079430'; + setTimeout(() => { + this.hideCorrect(); + }, 1500); + } + + hideCorrect() { + console.log('Hide correct'); + let elements = document.getElementsByClassName('flag'); + for (let item of elements) { + ReactDOM.findDOMNode(item).style.border = '3px dotted transparent'; + } + } + + submitScore(score, sessionTimer) { + const res = axios.post(api.url+'/api/flags/scores', { 'score' : score, 'sessionTimer' : sessionTimer, 'answers' : this.answers }); + this.answers = []; + console.log(res); + } + + componentDidMount() { + window.innerWidth = 500; + this.startGame(); + // this.foo(); + } + componentWillUnmount() { + this.gameOver(); + } + + + restartGame() { + console.log('RESTART'); + this.props.dispatch({type : 'reset'}); + this.gameOver(); + this.startGame(); + } + + answers = []; + question = []; + prepareStat() { + // console.log('FOO'); + // console.log(this.props); + // console.log(this.props.flags); + this.props.flagi.map((item) => + // () => alert() + this.question.push(item) + // console.log(item) + // (item) => { alert(); console.log('xaxa' + item)} + ); + + + // this.array.map(item => clearInterval(item)); + + + // console.log(); + // for (let item of this.props.flagi) { + // this.question.push(item.getAllKeys()[0]) + // } + // console.log(this.question); + } + + saveAnswer(correct) { + let answer = { + correct : correct, + answerCode : this.props.answerCode, + options : this.question.filter((item) => item !== this.props.answerCode), + time : this.props.maxTimer - this.props.timer + } + + this.answers.push(answer); + this.question = []; + console.log(this.answers); + } + + render() { + if (this.state.loading) { + return
Fetching question...
; + } + + return ( +
+ {/*// */} + + + {/**/} + + + Question: + + +
+ Select the flag of + Time: {this.props.timer} + +
+ +

{this.props.ques}

+ + { + this.props.flags.map(item => + ( + this.answer(item)}> + { + this.props.answer == item + ? {item} + : {item} + } + + ) + ) + } + + + GAME OVER! Your score: {this.props.counter} + + +
+ + {this.props.text}  + + Total time: {this.props.sessionTimer} +
+
+ {this.props.lifesIcon} + Score: {this.props.counter} +
+ +
+
+ {/**/} + +
+
+   + +
+
+ ) +} + redirect = () => { + this.gameOver(); + this.props.dispatch({type : 'reset'}); + this.props.history.push('/'); + } + + exitGame = () => { + this.gameOver(); + this.props.dispatch({type : 'reset'}); + this.props.history.push('/'); + } +} + +function mapStateToProps (state) { + return { + counter: state.add.counter, + text : state.add.text, + flagi : state ? Object.keys(state.add.flags) : {}, + flags : state ? Object.values(state.add.flags) : {}, + ques : state.add.ques, + answer : state.add.answer, + answerCode : state.add.answerCode, + token : state.add.token, + lifes : state.add.lifes, + lifesIcon : state.add.lifesIcon, + timer: state.add.timer, + interval : state.add.interval, + maxTimer: state.add.maxTimer, + sessionTimer: state.add.sessionTimer, + } +} + +export default connect(mapStateToProps)(FlagsApi); diff --git a/src/components/flags/Profile.js b/src/components/flags/Profile.js index a9100ac..33aeef6 100644 --- a/src/components/flags/Profile.js +++ b/src/components/flags/Profile.js @@ -9,6 +9,7 @@ import Tab from "react-bootstrap/Tab"; import Tabs from "react-bootstrap/Tabs"; import Alert from "react-bootstrap/Alert"; import { useHistory } from 'react-router-dom'; +import "../home/styles.css"; const Profile = () => { const history = useHistory(); @@ -102,18 +103,7 @@ const Profile = () => {
+ +
+
+ + )} + + {/* Hero Section */}
+ {isLoggedIn && ( + + )}

🏁 Flags Quiz

Test your geography knowledge and compete for the top spot!

{isLoggedIn ? ( @@ -185,7 +235,7 @@ const Home = () => {
- + ) } diff --git a/src/components/home/styles.css b/src/components/home/styles.css index 051a3a2..cbdbe83 100644 --- a/src/components/home/styles.css +++ b/src/components/home/styles.css @@ -1,3 +1,73 @@ +/* Page wrapper for sticky footer */ +.page-wrapper { + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.main-content { + flex: 1; +} + +/* Close/Logout button */ +.close-btn { + position: absolute; + top: 10px; + right: 15px; + background: none; + border: none; + font-size: 2rem; + font-weight: bold; + cursor: pointer; + opacity: 0.5; + line-height: 1; + color: #343a40; + transition: opacity 0.2s ease; +} + +.close-btn:hover { + opacity: 1; +} + +/* Logout Popup */ +.popup-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.4); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + backdrop-filter: blur(2px); +} + +.popup-content { + background: #fff; + padding: 1.5rem 2.5rem; + border-radius: 8px; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15); + text-align: center; +} + +.popup-content p { + margin: 0 0 1.25rem 0; + font-size: 1.1rem; + color: #343a40; +} + +.popup-buttons { + display: flex; + gap: 0.75rem; + justify-content: center; +} + +.popup-buttons .btn { + min-width: 70px; +} + /* Hero Section */ .home-hero { text-align: center; @@ -5,6 +75,7 @@ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); border-radius: 0.5rem; margin-bottom: 0; + position: relative; } .home-hero h1 { @@ -132,7 +203,7 @@ background-color: #1a1a2e; color: #8b8b9e; padding: 1.5rem 0; - margin-top: 3rem; + margin-top: auto; font-size: 0.7rem; } diff --git a/src/reducers/initial.js b/src/reducers/initial.js index 28ea8f7..220db51 100644 --- a/src/reducers/initial.js +++ b/src/reducers/initial.js @@ -136,10 +136,24 @@ const initial = (state, action) => { } } -export default (state = () => - new Map({ - counter : 0 - }), action) => { +const defaultState = { + 'counter' : 0, + 'text' : 'no data', + 'flags' : {}, + 'flagi' : {}, + 'ques' : 'ques', + 'answer' : '-', + 'answerCode' : '', + 'token' : '', + 'lifes' : 3, + 'lifesIcon' : '🟢🟢🟢', + 'timer' : 15, + 'maxTimer' : 15, + 'interval' : '', + 'sessionTimer' : 0, +}; + +export default (state = defaultState, action) => { switch (action.type) { case 'add': return add(state); case 'initial': return initial(state); From 6ab6bb7a4ea29fd7fb537f379cb138ddd24ee2fc Mon Sep 17 00:00:00 2001 From: "m::r" Date: Wed, 14 Jan 2026 01:18:05 +0000 Subject: [PATCH 4/4] chore(polishing) --- .dockerignore | 35 +++++++--- .env.production | 4 ++ .github/workflows/pipeline.yml | 2 + Caddyfile | 24 +++---- Dockerfile | 65 ++++++++++++------- Dockerfile.bkp | 41 ++++++++++++ Makefile | 113 +++++++++++++++++---------------- docker-compose.local.prod.yml | 12 ++++ docker-compose.local.yml | 10 ++- docker-compose.yml | 32 ++++++++-- docs/ENV_VARS.md | 41 ++++++++++++ src/components/home/Home.js | 6 +- src/components/home/styles.css | 1 - 13 files changed, 282 insertions(+), 104 deletions(-) create mode 100644 .env.production create mode 100644 Dockerfile.bkp create mode 100644 docker-compose.local.prod.yml create mode 100644 docs/ENV_VARS.md diff --git a/.dockerignore b/.dockerignore index ca11906..ba9a3c0 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,11 +1,30 @@ -node_modules -build -.git -.gitignore -.docker -*.md -.env* -.DS_Store +# --- Dependencies --- +node_modules/ npm-debug.log* yarn-debug.log* yarn-error.log* + +# --- Build Artifacts --- +build/ +dist/ +.cache/ + +# --- Environment & Secrets --- +.env* + +# --- Docker Files --- +# We ignore these because they aren't needed inside the image +Dockerfile +Dockerfile.* +docker-compose.yml +docker-compose.*.yml +.dockerignore +.docker +# --- Git --- +.git +.gitignore + +# --- OS Junk --- +.DS_Store +Thumbs.db +*.md diff --git a/.env.production b/.env.production new file mode 100644 index 0000000..6099f96 --- /dev/null +++ b/.env.production @@ -0,0 +1,4 @@ +# Production environment +# Used by `yarn build` locally and as defaults in Dockerfile +REACT_APP_API_URL=https://api.flags.izeebot.top +REACT_APP_AUTH_URL=https://auth.izeebot.top diff --git a/.github/workflows/pipeline.yml b/.github/workflows/pipeline.yml index d865521..999bc37 100644 --- a/.github/workflows/pipeline.yml +++ b/.github/workflows/pipeline.yml @@ -109,6 +109,8 @@ jobs: labels: ${{ steps.meta.outputs.labels }} build-args: | APP_VERSION=v${{ needs.version.outputs.new_version }} + REACT_APP_API_URL=${{ vars.REACT_APP_API_URL }} + REACT_APP_AUTH_URL=${{ vars.REACT_APP_AUTH_URL }} cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:buildcache,mode=max diff --git a/Caddyfile b/Caddyfile index 7bd90f8..bc99372 100644 --- a/Caddyfile +++ b/Caddyfile @@ -1,23 +1,25 @@ :80 { - root * /srv + root * /usr/share/caddy - # Enable gzip compression - encode gzip + # Enable compression + encode zstd gzip - # SPA fallback - serve index.html for client-side routing - try_files {path} /index.html - - file_server - - # Cache static assets + # 1. Hashed Assets: Cache forever + # IMPROVEMENT: Match files inside /static/ folders specifically + # CRA/Vite usually puts hashed assets in /static/js or /assets/ @static { path *.js *.css *.png *.jpg *.jpeg *.gif *.ico *.svg *.woff *.woff2 } header @static Cache-Control "public, max-age=31536000, immutable" - # Don't cache index.html + # 2. HTML & Service Workers: Never cache @html { - path *.html + path *.html sw.js } header @html Cache-Control "no-cache, no-store, must-revalidate" + + # SPA fallback + try_files {path} /index.html + + file_server } diff --git a/Dockerfile b/Dockerfile index 2e4d2f2..0d57285 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,36 +1,57 @@ -# Stage 1: Build the React application -FROM node:18-alpine AS builder +# --- Stage 1: Base --- +FROM node:18-alpine AS base + +# Receive IDs from docker-compose +ARG USER_ID=1000 +ARG GROUP_ID=1000 + +# Alpine-specific way to adjust 'node' user to match your host +RUN apk add --no-cache shadow && \ + if [ ${USER_ID:-0} -ne 0 ] && [ ${GROUP_ID:-0} -ne 0 ]; then \ + userdel -f node && \ + if getent group node ; then groupdel node; fi && \ + groupadd -g ${GROUP_ID} node && \ + useradd -l -u ${USER_ID} -g node node && \ + install -d -m 0755 -o node -g node /app; \ + fi WORKDIR /app +# From now on, the container acts as "you" +USER node -# Copy package files -COPY package.json yarn.lock* package-lock.json* ./ +# 3. Copy dependency files first (for caching) +COPY --chown=node:node package.json yarn.lock* package-lock.json* ./ -# Install dependencies +# Use frozen-lockfile for consistency RUN if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \ elif [ -f package-lock.json ]; then npm ci; \ else npm install; fi +# --- Stage 2: Development (Hot Reload) --- +FROM base AS development +ENV NODE_OPTIONS=--openssl-legacy-provider +# No COPY . . here! We use bind mounts in docker-compose. +EXPOSE 3000 +CMD ["npm", "start"] -# Copy source code +# --- Stage 3: Builder (Compilation) --- +FROM base AS builder COPY . . - -# Build argument for version (passed from CI/CD pipeline) -ARG APP_VERSION="" +# Args to bake URLs into the JS bundle +ARG REACT_APP_API_URL +ARG REACT_APP_AUTH_URL +ENV REACT_APP_API_URL=$REACT_APP_API_URL +ENV REACT_APP_AUTH_URL=$REACT_APP_AUTH_URL ENV NODE_OPTIONS=--openssl-legacy-provider -ENV REACT_APP_VERSION=${APP_VERSION} - -# Build the production bundle with version baked in RUN if [ -f yarn.lock ]; then yarn build; else npm run build; fi -# Stage 2: Serve with Caddy -FROM caddy:2-alpine - -# Copy built files from builder stage -COPY --from=builder /app/build /srv - -# Copy Caddyfile +# --- Stage 4: Production (Caddy) --- +FROM caddy:2-alpine AS production +# Set the working directory for Caddy +WORKDIR /usr/share/caddy +# Copy built files from the builder stage +COPY --from=builder /app/build /usr/share/caddy +# Copy your optimized Caddyfile COPY Caddyfile /etc/caddy/Caddyfile - EXPOSE 80 - -CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"] +# Use the config file we just copied +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile", "--adapter", "caddyfile"] \ No newline at end of file diff --git a/Dockerfile.bkp b/Dockerfile.bkp new file mode 100644 index 0000000..6966e84 --- /dev/null +++ b/Dockerfile.bkp @@ -0,0 +1,41 @@ +# Stage 1: Build the React application +FROM node:18-alpine AS builder + +WORKDIR /app + +# Copy package files +COPY package.json yarn.lock* package-lock.json* ./ + +# Install dependencies +RUN if [ -f yarn.lock ]; then yarn install --frozen-lockfile; \ + elif [ -f package-lock.json ]; then npm ci; \ + else npm install; fi + +# Copy source code +COPY . . + +# Build arguments (passed from CI/CD pipeline) +ARG APP_VERSION="" +ARG REACT_APP_API_URL=https://api.flags.izeebot.top +ARG REACT_APP_AUTH_URL=https://auth.izeebot.top + +ENV NODE_OPTIONS=--openssl-legacy-provider +ENV REACT_APP_VERSION=${APP_VERSION} +ENV REACT_APP_API_URL=${REACT_APP_API_URL} +ENV REACT_APP_AUTH_URL=${REACT_APP_AUTH_URL} + +# Build the production bundle with version baked in +RUN if [ -f yarn.lock ]; then yarn build; else npm run build; fi + +# Stage 2: Serve with Caddy +FROM caddy:2-alpine + +# Copy built files from builder stage +COPY --from=builder /app/build /srv + +# Copy Caddyfile +COPY Caddyfile /etc/caddy/Caddyfile + +EXPOSE 80 + +CMD ["caddy", "run", "--config", "/etc/caddy/Caddyfile"] diff --git a/Makefile b/Makefile index 6e93b07..7d8679f 100644 --- a/Makefile +++ b/Makefile @@ -1,66 +1,73 @@ -.PHONY: help build up down rebuild dev test install clean logs sh network +.PHONY: help network dev prod-test build-prod down clean-docker sh logs + +#CYAN := \033[0;36m +#RESET := \033[0m +# Use printf to set variables so they work with any 'echo' +CYAN := $(shell printf ' \033[0;36m') +RESET := $(shell printf ' \033[0m') + +# Settings +COMPOSE_DEV := docker compose +COMPOSE_PROD := docker compose -f docker-compose.yml -f docker-compose.local-prod.yml + +init: network ## First-time setup: Create networks, clean environment, and start dev + @echo "$(CYAN)Initializing Docker environment...$(RESET)" + @$(COMPOSE_DEV) down -v --remove-orphans + @echo "$(CYAN)Building and starting containers (this may take a few minutes)...$(RESET)" + @$(COMPOSE_DEV) up --build -d + @echo "$(CYAN)Installation complete!$(RESET)" + @echo "Checking container status..." + @$(COMPOSE_DEV) ps + @echo "Follow logs with: $(CYAN)make logs$(RESET)" + @echo "App will be available at: http://localhost:3000" -CYAN := \033[0;36m -RESET := \033[0m - -# Local compose file -LOCAL_COMPOSE := docker compose -f docker-compose.local.yml - -# Default target help: ## Show this help message - @printf "\\nUsage: make $(CYAN)[target]$(RESET)\\n\\n" - @echo 'Targets:' + @printf "\nUsage: make $(CYAN)[target]$(RESET)\n\n" @awk 'BEGIN {FS = ":.*?## "} /^[a-zA-Z_-]+:.*?## / {printf " $(CYAN)%-22s$(RESET) %s\n", $$1, $$2}' $(MAKEFILE_LIST) | sort -f -install: ## Install dependencies - @yarn install - -dev: ## Start development server at http://localhost:3000 - @echo "Starting dev server at http://localhost:3000" - @echo "Using API: $${REACT_APP_API_URL:-http://localhost:8000}" - @echo "Using Auth: $${REACT_APP_AUTH_URL:-http://localhost:8547}" - @yarn start - -build: ## Build production bundle - @yarn build - -test: ## Run tests - @yarn test - -# Docker targets -docker-build: ## Build local Docker image - @$(LOCAL_COMPOSE) build +network: ## Create required Docker networks + @docker network create backend-flags 2>/dev/null || true + @docker network create openid_network 2>/dev/null || true + @echo "Networks verified." -docker-up: ## Start local Docker container - @$(LOCAL_COMPOSE) up -d +# --- DEVELOPMENT --- +dev: network ## Start HOT-RELOAD development (Port 3000) + @echo "Starting Dev Server (Node 18)..." + @$(COMPOSE_DEV) up --build -docker-down: ## Stop local Docker container - @$(LOCAL_COMPOSE) down +# --- LOCAL PRODUCTION TESTING --- +prod-test: network ## Start LOCAL PROD container (Caddy, Port 3001) + @echo "Building & Starting Production-like container (Caddy)..." + @$(COMPOSE_PROD) up --build -d + @echo "App running at http://localhost:3001" -docker-rebuild: docker-down docker-build docker-up ## Rebuild and restart Docker container +# --- CI / REAL PRODUCTION --- +build-prod: ## Manually build the final production image (Caddy stage) + @docker build --target production -t flags-app:latest . -docker-logs: ## Show Docker container logs - @$(LOCAL_COMPOSE) logs -f +# --- UTILS --- +down: ## Stop all local containers + @$(COMPOSE_DEV) down --remove-orphans + @$(COMPOSE_PROD) down --remove-orphans -docker-sh: ## Shell into Docker container - @$(LOCAL_COMPOSE) exec app sh +sh: ## Drop into the running dev container shell + @$(COMPOSE_DEV) exec react sh -# Full local setup -local: docker-build docker-up ## Build and start local Docker setup - @echo "App running at http://localhost:3001" +logs: ## Tail logs from the dev container + @$(COMPOSE_DEV) logs -f react -# Network setup -network: ## Create required Docker networks - @docker network create backend-flags 2>/dev/null || true - @docker network create openid_network 2>/dev/null || true - @echo "Networks created (or already exist)" +clean-docker: ## Nuclear option: remove volumes and orphans + @$(COMPOSE_DEV) down -v --remove-orphans + @docker system prune -f --filter "label=com.docker.compose.project" + @echo "Docker environment cleaned." -# Cleanup -clean: ## Clean build artifacts - @rm -rf build/ - @rm -rf node_modules/.cache/ - @echo "Cleaned build artifacts" +reinstall: ## Wipe container modules and reinstall from scratch + @echo "Hard resetting container dependencies..." + @$(COMPOSE_DEV) down -v + @$(COMPOSE_DEV) build --no-cache + @$(COMPOSE_DEV) up -d + @echo "Fresh dependencies installed inside the container." -clean-all: clean ## Clean all (including node_modules) - @rm -rf node_modules/ - @echo "Cleaned node_modules" +npm-add: ## Add a new package (Usage: make npm-add PKG=axios) + @$(COMPOSE_DEV) exec react npm install $(PKG) + @echo "Package $(PKG) added and package.json updated via container." diff --git a/docker-compose.local.prod.yml b/docker-compose.local.prod.yml new file mode 100644 index 0000000..f1bac1e --- /dev/null +++ b/docker-compose.local.prod.yml @@ -0,0 +1,12 @@ +services: + react: + build: + target: production # <--- Switches to the Caddy stage + args: + # Bakes your LOCAL ports into the production build + - REACT_APP_API_URL=${REACT_APP_API_URL:-http://localhost:3001} + - REACT_APP_AUTH_URL=${REACT_APP_AUTH_URL:-http://localhost:8547} + container_name: flags-local-prod + ports: + - "3001:80" # Run on 3001 so it doesn't clash with your dev server + volumes: !reset [] # Wipes the dev volumes (no code mounting!) \ No newline at end of file diff --git a/docker-compose.local.yml b/docker-compose.local.yml index de73558..01992bf 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -8,10 +8,18 @@ services: REACT_APP_AUTH_URL: ${REACT_APP_AUTH_URL:-http://localhost:8547} container_name: flagsapp-local ports: - - "3001:80" + - "3000:3000" networks: - backend-flags - openid_network + volumes: + - .:/app + - /app/node_modules # Anonymous volume to protect container modules + environment: + - CHOKIDAR_USEPOLLING=true # Essential for HMR on some OS (Fedora/Windows) + - WATCHPACK_POLLING=true # For newer Webpack/Vite versions + stdin_open: true # Keeps the terminal active + tty: true networks: backend-flags: diff --git a/docker-compose.yml b/docker-compose.yml index ae68016..71d68a4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,29 @@ services: - flagsapp: - build: . - container_name: flagsapp + react: + build: + context: . + args: + USER_ID: 1000 + GROUP_ID: 1000 + target: development # <--- Forces it to stop at the Node dev server + container_name: flags-dev ports: - - "80:80" - restart: unless-stopped + - "3000:3000" + volumes: + - .:/app:rw,Z + - /app/node_modules # Prevents host modules from breaking container + environment: + - HOST=0.0.0.0 + - CHOKIDAR_USEPOLLING=true # Essential for Linux hot-reload + - WATCHPACK_POLLING=true # Double-down on polling for CRA/Webpack + - REACT_APP_API_URL=http://localhost:8000 + - REACT_APP_AUTH_URL=http://localhost:8547 + networks: + - backend-flags + - openid_network + +networks: + backend-flags: + external: true + openid_network: + external: true \ No newline at end of file diff --git a/docs/ENV_VARS.md b/docs/ENV_VARS.md new file mode 100644 index 0000000..3e83f91 --- /dev/null +++ b/docs/ENV_VARS.md @@ -0,0 +1,41 @@ + Summary: How It Works Now + + For local development (yarn start) + + React picks up from: .env.development → http://localhost:8000 + + For local production build (yarn build) + + React picks up from: .env.production → https://api.flags.izeebot.top + + For Docker builds + + Local dev (Dockerfile.local): + docker build -f Dockerfile.local \ + --build-arg REACT_APP_API_URL=http://localhost:8000 \ + --build-arg REACT_APP_AUTH_URL=http://localhost:8547 \ + -t flagsapp-local . + + Production (Dockerfile via pipeline): + - Uses GitHub repository variables (vars.REACT_APP_API_URL) + - Falls back to Dockerfile defaults if not set + + Action Required: Add GitHub Repository Variables + + Go to GitHub → Repo Settings → Secrets and variables → Actions → Variables and add: + ┌────────────────────┬───────────────────────────────┐ + │ Variable │ Value │ + ├────────────────────┼───────────────────────────────┤ + │ REACT_APP_API_URL │ https://api.flags.izeebot.top │ + ├────────────────────┼───────────────────────────────┤ + │ REACT_APP_AUTH_URL │ https://auth.izeebot.top │ + └────────────────────┴───────────────────────────────┘ + Using vars (variables) instead of secrets since these aren't sensitive - they're just URLs that end up in the JS bundle anyway. + + --- + Key Points About React + .env Files + + 1. CRA loads .env files at build time, not runtime + 2. Only REACT_APP_* prefixed vars are embedded + 3. Values get baked into the JS bundle - cannot change without rebuild + 4. Priority: .env.development.local > .env.local > .env.development > .env diff --git a/src/components/home/Home.js b/src/components/home/Home.js index ceccc7e..29d8039 100644 --- a/src/components/home/Home.js +++ b/src/components/home/Home.js @@ -102,10 +102,10 @@ const Home = () => { )} - + {/* Hero Section */} - - + +
{isLoggedIn && (