Add VITE_APP_VERSION environment variable and integrate versioning into the application

- Introduced VITE_APP_VERSION in Dockerfile and GitHub workflows to capture the version from Git tags.
- Updated vite.config.ts to define a global constant for the app version.
- Enhanced Layout component to display the app version in the footer for admin pages.
- Added logic to conditionally render the LanguageSwitcher based on active promotions.
This commit is contained in:
PEDZEO
2026-01-19 07:17:49 +03:00
parent a261a5143f
commit 5409317501
6 changed files with 73 additions and 21 deletions

View File

@@ -62,3 +62,4 @@ jobs:
VITE_TELEGRAM_BOT_USERNAME= VITE_TELEGRAM_BOT_USERNAME=
VITE_APP_NAME=Cabinet VITE_APP_NAME=Cabinet
VITE_APP_LOGO=V VITE_APP_LOGO=V
VITE_APP_VERSION=${{ github.ref_name }}

View File

@@ -34,6 +34,7 @@ jobs:
VITE_TELEGRAM_BOT_USERNAME: VITE_TELEGRAM_BOT_USERNAME:
VITE_APP_NAME: Bedolaga Cabinet VITE_APP_NAME: Bedolaga Cabinet
VITE_APP_LOGO: V VITE_APP_LOGO: V
VITE_APP_VERSION: ${{ github.ref_name }}
- name: Create dist archive - name: Create dist archive
run: | run: |

View File

@@ -18,12 +18,14 @@ ARG VITE_API_URL=/api
ARG VITE_TELEGRAM_BOT_USERNAME ARG VITE_TELEGRAM_BOT_USERNAME
ARG VITE_APP_NAME=Cabinet ARG VITE_APP_NAME=Cabinet
ARG VITE_APP_LOGO=V ARG VITE_APP_LOGO=V
ARG VITE_APP_VERSION
# Set environment variables for build # Set environment variables for build
ENV VITE_API_URL=$VITE_API_URL ENV VITE_API_URL=$VITE_API_URL
ENV VITE_TELEGRAM_BOT_USERNAME=$VITE_TELEGRAM_BOT_USERNAME ENV VITE_TELEGRAM_BOT_USERNAME=$VITE_TELEGRAM_BOT_USERNAME
ENV VITE_APP_NAME=$VITE_APP_NAME ENV VITE_APP_NAME=$VITE_APP_NAME
ENV VITE_APP_LOGO=$VITE_APP_LOGO ENV VITE_APP_LOGO=$VITE_APP_LOGO
ENV VITE_APP_VERSION=$VITE_APP_VERSION
# Build the application # Build the application
RUN npm run build RUN npm run build

View File

@@ -11,6 +11,7 @@ import { pollsApi } from '../../api/polls'
import { brandingApi } from '../../api/branding' import { brandingApi } from '../../api/branding'
import { wheelApi } from '../../api/wheel' import { wheelApi } from '../../api/wheel'
import { themeColorsApi } from '../../api/themeColors' import { themeColorsApi } from '../../api/themeColors'
import { promoApi } from '../../api/promo'
import { useTheme } from '../../hooks/useTheme' import { useTheme } from '../../hooks/useTheme'
// Fallback branding from environment variables // Fallback branding from environment variables
@@ -210,6 +211,17 @@ export default function Layout({ children }: LayoutProps) {
retry: false, retry: false,
}) })
// Fetch active discount to determine mobile layout
const { data: activeDiscount } = useQuery({
queryKey: ['active-discount'],
queryFn: promoApi.getActiveDiscount,
enabled: isAuthenticated,
staleTime: 30000,
})
// Check if promo is active (to hide language switcher on mobile)
const isPromoActive = activeDiscount?.is_active && activeDiscount?.discount_percent
const navItems = useMemo(() => { const navItems = useMemo(() => {
const items = [ const items = [
{ path: '/', label: t('nav.dashboard'), icon: HomeIcon }, { path: '/', label: t('nav.dashboard'), icon: HomeIcon },
@@ -339,7 +351,10 @@ export default function Layout({ children }: LayoutProps) {
<PromoDiscountBadge /> <PromoDiscountBadge />
<TicketNotificationBell isAdmin={isAdminActive()} /> <TicketNotificationBell isAdmin={isAdminActive()} />
<LanguageSwitcher /> {/* Hide language switcher on mobile when promo is active */}
<div className={isPromoActive ? 'hidden sm:block' : ''}>
<LanguageSwitcher />
</div>
{/* Profile - Desktop */} {/* Profile - Desktop */}
<div className="hidden sm:flex items-center gap-3"> <div className="hidden sm:flex items-center gap-3">
@@ -389,29 +404,33 @@ export default function Layout({ children }: LayoutProps) {
<div className="absolute inset-x-0 top-0 bottom-0 bg-dark-900 border-t border-dark-800/50 overflow-y-auto pb-[calc(5rem+env(safe-area-inset-bottom,0px))]"> <div className="absolute inset-x-0 top-0 bottom-0 bg-dark-900 border-t border-dark-800/50 overflow-y-auto pb-[calc(5rem+env(safe-area-inset-bottom,0px))]">
<div className="max-w-6xl mx-auto px-4 py-4"> <div className="max-w-6xl mx-auto px-4 py-4">
{/* User info */} {/* User info */}
<div className="flex items-center gap-3 pb-4 mb-4 border-b border-dark-800/50"> <div className="flex items-center justify-between pb-4 mb-4 border-b border-dark-800/50">
{userPhotoUrl ? ( <div className="flex items-center gap-3">
<img {userPhotoUrl ? (
src={userPhotoUrl} <img
alt="Avatar" src={userPhotoUrl}
className="w-10 h-10 rounded-full object-cover" alt="Avatar"
onError={(e) => { className="w-10 h-10 rounded-full object-cover"
e.currentTarget.style.display = 'none' onError={(e) => {
e.currentTarget.nextElementSibling?.classList.remove('hidden') e.currentTarget.style.display = 'none'
}} e.currentTarget.nextElementSibling?.classList.remove('hidden')
/> }}
) : null} />
<div className={`w-10 h-10 rounded-full bg-dark-700 flex items-center justify-center ${userPhotoUrl ? 'hidden' : ''}`}> ) : null}
<UserIcon /> <div className={`w-10 h-10 rounded-full bg-dark-700 flex items-center justify-center ${userPhotoUrl ? 'hidden' : ''}`}>
</div> <UserIcon />
<div>
<div className="text-sm font-medium text-dark-100">
{user?.first_name || user?.username}
</div> </div>
<div className="text-xs text-dark-500"> <div>
@{user?.username || `ID: ${user?.telegram_id}`} <div className="text-sm font-medium text-dark-100">
{user?.first_name || user?.username}
</div>
<div className="text-xs text-dark-500">
@{user?.username || `ID: ${user?.telegram_id}`}
</div>
</div> </div>
</div> </div>
{/* Language switcher in mobile menu when promo is active */}
{isPromoActive && <LanguageSwitcher />}
</div> </div>
{/* Nav items */} {/* Nav items */}
@@ -480,6 +499,15 @@ export default function Layout({ children }: LayoutProps) {
<div className="animate-fade-in"> <div className="animate-fade-in">
{children} {children}
</div> </div>
{/* Version footer for admin pages */}
{isAdminActive() && (
<div className="mt-8 pt-4 border-t border-dark-800/50 text-center">
<span className="text-xs text-dark-500">
Cabinet {__APP_VERSION__}
</span>
</div>
)}
</main> </main>
{/* Mobile Bottom Navigation - only core items */} {/* Mobile Bottom Navigation - only core items */}

3
src/vite-env.d.ts vendored
View File

@@ -1,5 +1,8 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
// Global constant defined in vite.config.ts
declare const __APP_VERSION__: string
interface ImportMetaEnv { interface ImportMetaEnv {
readonly VITE_API_URL: string readonly VITE_API_URL: string
readonly VITE_TELEGRAM_BOT_USERNAME?: string readonly VITE_TELEGRAM_BOT_USERNAME?: string

View File

@@ -1,9 +1,26 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react'
import { execSync } from 'child_process'
// Get version from Git tag or fallback to 'dev'
function getGitVersion(): string {
try {
// Try to get version from git tag
return execSync('git describe --tags --always', { encoding: 'utf-8' }).trim()
} catch {
return 'dev'
}
}
const appVersion = process.env.VITE_APP_VERSION || getGitVersion()
// https://vitejs.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
// Define global constants
define: {
__APP_VERSION__: JSON.stringify(appVersion),
},
// Base path - use '/' for standalone Docker deployment // Base path - use '/' for standalone Docker deployment
// Change to '/cabinet/' if serving from a sub-path // Change to '/cabinet/' if serving from a sub-path
base: '/', base: '/',