Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Dependencies
node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Testing
coverage
tests

# IDE
.vscode
.idea
*.swp
*.swo
*~

# OS
.DS_Store
Thumbs.db

# Git
.git
.gitignore
.gitattributes

# CI/CD
.github

# Documentation
*.md
LICENSE.txt

# Build artifacts (будут созданы внутри контейнера)
dist
build
*.log

# Config files (не нужны в образе)
.prettierrc.js
.prettierignore
.eslintignore
.lintstagedrc.json
eslint.config.js
tsconfig*.json
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .dockerignore file excludes tsconfig*.json files, but these are typically required for the TypeScript build process. The Dockerfile runs 'npm run build --prefix web' which likely needs these configuration files. Consider removing 'tsconfig*.json' from .dockerignore or verify that the build works without these files.

Suggested change
tsconfig*.json

Copilot uses AI. Check for mistakes.
vite.config.ts
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .dockerignore file excludes vite.config.ts, which is essential for the Vite build process. The Dockerfile runs 'npm run build --prefix web' which requires this configuration file. Remove 'vite.config.ts' from .dockerignore to ensure the build works correctly.

Suggested change
vite.config.ts

Copilot uses AI. Check for mistakes.

# Native app (не нужно в web образе)
native

# Storybook
.storybook
*.stories.tsx

# Environment (будет встроено в build)
.env.local
.env.development
.env.production

# Husky
.husky

# Misc
*.iml
3 changes: 0 additions & 3 deletions .env
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
IJO42_URL = ijo42.ru

VITE_MAPTILES_STYLE_KEY = 1XfSivF5uaaJV0EiuRS1

VITE_URL_IJO42_TILES = martin://tiles2.$IJO42_URL/
VITE_URL_MAP_ASSETS = https://res.$IJO42_URL/
VITE_URL_IJO42_MAPI = https://mapi.$IJO42_URL/v2
VITE_URL_PSU_TOOLS_API = https://events.$IJO42_URL
VITE_URL_ICAL_ENDPOINT = https://ical.psu.ru/calendars/

VITE_URL_MAPTILER_STYLE = https://api.maptiler.com/maps/streets/style.json
VITE_URL_BIND_ETIS = https://student.psu.ru/
VITE_URL_TG_GROUP = https://t.me/psumaps
VITE_URL_SUPPORT = https://t.me/psumaps_sbot?start=vkmapp
Expand Down
86 changes: 86 additions & 0 deletions .github/workflows/docker-build.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: Build and Push Docker Image

on:
push:
branches:
- master
tags:
- 'v*'
pull_request:
branches:
- master

env:
REGISTRY: ghcr.io
IMAGE_NAME: psumaps/mini-app

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write

steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Load environment variables from .env
id: dotenv
run: |
# Parse .env file and create build-args
echo "build_args<<EOF" >> $GITHUB_OUTPUT

# First, extract IJO42_URL for variable expansion
IJO42_URL=$(grep "^IJO42_URL" .env | cut -d= -f2 | xargs)
export IJO42_URL

# Process all VITE_* variables
grep "^VITE_" .env | while IFS= read -r line; do
key=$(echo "$line" | cut -d= -f1 | xargs)
value=$(echo "$line" | cut -d= -f2- | xargs)

# Expand variables (like $IJO42_URL)
expanded_value=$(eval echo "$value")
echo "$key=$expanded_value" >> $GITHUB_OUTPUT
done

echo "EOF" >> $GITHUB_OUTPUT

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels)
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}

- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: ${{ steps.dotenv.outputs.build_args }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Image digest
run: echo ${{ steps.build.outputs.digest }}
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow references 'steps.build.outputs.digest' but the 'Build and push Docker image' step doesn't have an 'id' field. Add 'id: build' to the 'Build and push Docker image' step to make this reference work, or remove this line if the digest output is not needed.

Copilot uses AI. Check for mistakes.
56 changes: 56 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
FROM node:20-alpine AS builder

WORKDIR /app

# Copy package files
COPY package*.json ./
COPY shared/package*.json ./shared/
COPY web/package*.json ./web/

# Install dependencies
RUN npm ci && \
npm ci --prefix shared && \
npm ci --prefix web

# Copy source code
COPY . .

# Build arguments for environment variables
ARG VITE_URL_IJO42_TILES
ARG VITE_URL_MAP_ASSETS
ARG VITE_URL_IJO42_MAPI
ARG VITE_URL_PSU_TOOLS_API
ARG VITE_URL_ICAL_ENDPOINT
ARG VITE_URL_BIND_ETIS
ARG VITE_URL_TG_GROUP
ARG VITE_URL_SUPPORT
ARG VITE_URL_VK_APP

# Set environment variables for build
ENV VITE_URL_IJO42_TILES=${VITE_URL_IJO42_TILES}
ENV VITE_URL_MAP_ASSETS=${VITE_URL_MAP_ASSETS}
ENV VITE_URL_IJO42_MAPI=${VITE_URL_IJO42_MAPI}
ENV VITE_URL_PSU_TOOLS_API=${VITE_URL_PSU_TOOLS_API}
ENV VITE_URL_ICAL_ENDPOINT=${VITE_URL_ICAL_ENDPOINT}
ENV VITE_URL_BIND_ETIS=${VITE_URL_BIND_ETIS}
ENV VITE_URL_TG_GROUP=${VITE_URL_TG_GROUP}
ENV VITE_URL_SUPPORT=${VITE_URL_SUPPORT}
ENV VITE_URL_VK_APP=${VITE_URL_VK_APP}

# Build the web application
RUN npm run build --prefix web

FROM nginxinc/nginx-unprivileged:alpine

# Copy custom nginx configuration
COPY nginx.conf /etc/nginx/conf.d/default.conf

# Copy built assets from builder stage
COPY --from=builder /app/web/dist /usr/share/nginx/html

# nginx-unprivileged runs on port 8080 by default
EXPOSE 8080

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
23 changes: 23 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
services:
psumaps-web:
build:
context: .
dockerfile: Dockerfile
args:
VITE_URL_IJO42_TILES: martin://tiles2.ijo42.ru/
VITE_URL_MAP_ASSETS: https://res.ijo42.ru/
VITE_URL_IJO42_MAPI: https://mapi.ijo42.ru/v2
VITE_URL_ICAL_ENDPOINT: https://ical.psu.ru/calendars/
VITE_URL_BIND_ETIS: https://student.psu.ru/
VITE_URL_TG_GROUP: https://t.me/psumaps
VITE_URL_SUPPORT: https://t.me/psumaps_sbot?start=vkmapp
VITE_URL_VK_APP: https://m.vk.com/app51764300
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Dockerfile defines build arguments VITE_URL_PSU_TOOLS_API (line 22) and the vite-env.d.ts requires both VITE_PSU_TOOLS_KEY and VITE_URL_PSU_TOOLS_API, but the docker-compose.yaml file doesn't provide either of these build arguments. This will cause builds using docker-compose to have undefined environment variables. Either add these variables to docker-compose.yaml or remove them from the Dockerfile if they're optional.

Suggested change
VITE_URL_VK_APP: https://m.vk.com/app51764300
VITE_URL_VK_APP: https://m.vk.com/app51764300
VITE_URL_PSU_TOOLS_API: ${VITE_URL_PSU_TOOLS_API}
VITE_PSU_TOOLS_KEY: ${VITE_PSU_TOOLS_KEY}

Copilot uses AI. Check for mistakes.
ports:
- "8080:8080"
restart: unless-stopped
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 5s
53 changes: 53 additions & 0 deletions nginx.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
server {
listen 8080;
server_name localhost;
root /usr/share/nginx/html;
index index.html;

# SPA routing - always serve index.html for client-side routing
location / {
try_files $uri $uri/ /index.html;
}

# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}

# Disable caching for index.html to ensure users get latest version
location = /index.html {
add_header Cache-Control "no-cache, no-store, must-revalidate";
expires 0;
}

# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
Copy link

Copilot AI Feb 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The nginx configuration lacks a Content-Security-Policy header, which is an important security measure for web applications. Consider adding a CSP header to protect against XSS attacks and other code injection vulnerabilities. For example: 'add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;'

Suggested change
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;

Copilot uses AI. Check for mistakes.

# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_comp_level 6;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/json
application/javascript
application/xml
application/xml+rss
application/x-javascript
image/svg+xml;

# Health check endpoint for k8s probes
location /health {
access_log off;
return 200 "OK\n";
add_header Content-Type text/plain;
}
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "psumaps-frontend",
"version": "1.1.1",
"version": "1.2.0",
"description": "Client applications of PSU Maps",
"type": "module",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion shared/.storybook/preview.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {withThemeByClassName} from '@storybook/addon-themes';
import { withThemeByClassName } from '@storybook/addon-themes';
import '../../web/src/tw.css';

/** @type { import('@storybook/react').Preview } */
Expand Down
8 changes: 4 additions & 4 deletions shared/src/components/map/searchPopUp/search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ const SEARCH_DEBOUNCE_MS = 500;
const Search = () => {
const { data: animEnabled } = useAnimEnabled();
const queryClient = useQueryClient();
const { token } = useIcalToken();
const { jwtToken } = useIcalToken();
const storage = useContext(StorageContext);

const { search, popupState, setPopupState, selectedPoi } =
Expand All @@ -45,22 +45,22 @@ const Search = () => {
const searchQuery = useQuery(
{
queryKey: ['search', debouncedSearch],
queryFn: async () => httpClient.mapi.search(debouncedSearch, token!),
queryFn: async () => httpClient.mapi.search(debouncedSearch, jwtToken!),
enabled: !!debouncedSearch && popupState === 'opened',
...queryOptions,
},
queryClient,
);
const amenities = useQuery({
queryKey: ['amenities'],
queryFn: async () => httpClient.mapi.getAmenityList(token!),
queryFn: async () => httpClient.mapi.getAmenityList(jwtToken!),
...queryOptions,
enabled: popupState === 'opened',
});
const amenityPois = useQuery({
queryKey: ['amenity-pois', selectedAmenity],
queryFn: async () =>
httpClient.mapi.getPoiByAmenity(selectedAmenity!, token!),
httpClient.mapi.getPoiByAmenity(selectedAmenity!, jwtToken!),
enabled: !!selectedAmenity && popupState === 'opened',
...queryOptions,
});
Expand Down
Loading
Loading