diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..32dfd32 --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +# Заголовок на главной карточке +VITE_APP_TITLE=Генератор QR-кодов + +# Плашка внизу: бренд, слоган и ссылка +VITE_BANNER_BRAND=iQR +VITE_BANNER_SLOGAN=— qr-код без излишеств +VITE_BANNER_URL=https://github.com/IliaMiheev/iQR + +# Акцентный цвет (hex, rgb или css-имя) +VITE_ACCENT_COLOR=#ff4500 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..d62b6c3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.sh text eol=lf +docker/entrypoint.sh text eol=lf diff --git a/.gitignore b/.gitignore index 2be163c..9ba33b0 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ node_modules dist .DS_Store *.local +.env __pycache__ ideas.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..a11ec8e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM node:22-alpine AS build + +WORKDIR /app + +COPY package.json package-lock.json ./ +RUN npm ci + +COPY . . +RUN npm run build + +FROM nginx:1.27-alpine + +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY docker/entrypoint.sh /entrypoint.sh +COPY --from=build /app/dist /usr/share/nginx/html + +RUN sed -i 's/\r$//' /entrypoint.sh && chmod +x /entrypoint.sh + +EXPOSE 80 + +ENTRYPOINT ["/bin/sh", "/entrypoint.sh"] diff --git a/Readme.md b/Readme.md index 478cd3d..fe2b7df 100644 --- a/Readme.md +++ b/Readme.md @@ -2,7 +2,7 @@ Вводишь текст и генерируешь. Всё просто. -Проект переписан на **Vue 3 + Vite**. +Проект на **Vue 3 + Vite**. Подходит для деплоя на поддомен (например `qr.example.com`) с кастомизацией брендинга. ### Запуск локально @@ -11,26 +11,66 @@ npm install npm run dev ``` -Приложение откроется по адресу `http://localhost:5173/iQR/`. +Приложение откроется по адресу `http://localhost:5173/`. -### Сборка для GitHub Pages +Файл `.env` **не обязателен** — без него работают значения по умолчанию. Нужен только если хотите кастомизировать интерфейс при локальной разработке (`npm run dev` / `npm run build`). + +### Настройка через `.env` (только локально) + +Скопируйте `.env.example` в `.env` и измените значения: + +| Переменная | Описание | По умолчанию | +|---|---|---| +| `VITE_APP_TITLE` | Заголовок | `Генератор QR-кодов` | +| `VITE_BANNER_BRAND` | Бренд в плашке | `iQR` | +| `VITE_BANNER_SLOGAN` | Слоган в плашке | `— qr-код без излишеств` | +| `VITE_BANNER_URL` | Ссылка плашки | репозиторий iQR | +| `VITE_ACCENT_COLOR` | Акцентный цвет | `#ff4500` | + +После изменения `.env` перезапустите `npm run dev`. + +### Docker + +Для Docker **`.env` не нужен** — настройки передаются переменными окружения в `docker-compose.yml`. + +```bash +docker compose up --build +``` + +Сайт будет на `http://localhost:8080`. + +```yaml +environment: + APP_TITLE: "QR на моём сайте" + BANNER_BRAND: "MySite" + BANNER_SLOGAN: "— быстрые qr-коды" + BANNER_URL: "https://example.com" + ACCENT_COLOR: "#7c3aed" +``` + +При старте контейнера генерируется `config.json`, поэтому пересборка образа для смены текстов не нужна. + +### Сборка ```bash npm run build ``` -Содержимое папки `dist/` нужно задеплоить на GitHub Pages (корень сайта — `dist`, base path уже настроен как `/iQR/`). +Результат в папке `dist/`. ### Структура ``` src/ - App.vue — главный компонент + App.vue + config/ — загрузка настроек composables/ - useTheme.js — тёмная/светлая тема - useQrCode.js — генерация, скачивание, масштаб - assets/style.css — стили -public/img/ — иконки, курсоры, favicon + useTheme.js + useQrCode.js + useAppConfig.js + assets/style.css +public/img/ +docker/ — nginx и entrypoint ```
diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..48626d0 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +services: + iqr: + build: . + ports: + - "8080:80" + environment: + APP_TITLE: "Генератор QR-кодов" + BANNER_BRAND: "iQR" + BANNER_SLOGAN: "— qr-код без излишеств" + BANNER_URL: "https://github.com/IliaMiheev/iQR" + ACCENT_COLOR: "#ff4500" + restart: unless-stopped diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh new file mode 100644 index 0000000..67a66bc --- /dev/null +++ b/docker/entrypoint.sh @@ -0,0 +1,20 @@ +#!/bin/sh +set -eu + +APP_TITLE="${APP_TITLE:-Генератор QR-кодов}" +BANNER_BRAND="${BANNER_BRAND:-iQR}" +BANNER_SLOGAN="${BANNER_SLOGAN:-— qr-код без излишеств}" +BANNER_URL="${BANNER_URL:-https://github.com/IliaMiheev/iQR}" +ACCENT_COLOR="${ACCENT_COLOR:-#ff4500}" + +cat > /usr/share/nginx/html/config.json < - + diff --git a/src/App.vue b/src/App.vue index 4cbff0a..b777cb8 100644 --- a/src/App.vue +++ b/src/App.vue @@ -2,7 +2,9 @@ import { computed, onMounted, onUnmounted, watch } from 'vue' import { useTheme } from './composables/useTheme' import { useQrCode } from './composables/useQrCode' +import { useAppConfig } from './composables/useAppConfig' +const config = useAppConfig() const { isLightTheme, toggleTheme } = useTheme() const { @@ -51,7 +53,7 @@ onUnmounted(() => {
+
- iQR - — qr-код без излишеств + {{ config.bannerBrand }} + {{ config.bannerSlogan }} diff --git a/src/assets/style.css b/src/assets/style.css index 4b5d11d..dd78d0e 100644 --- a/src/assets/style.css +++ b/src/assets/style.css @@ -1,5 +1,5 @@ ::selection { - background-color: orangered; + background-color: var(--accent); color: white; } @@ -10,7 +10,6 @@ html { html.dark { --bg: #1a1a1e; - --bg-accent: rgba(255, 69, 0, 0.06); --surface: rgba(38, 38, 42, 0.72); --surface-hover: rgba(52, 52, 58, 0.88); --surface-solid: rgba(30, 30, 34, 0.85); @@ -20,20 +19,23 @@ html.dark { --placeholder: rgba(255, 255, 255, 0.35); --shadow: 0 8px 32px rgba(0, 0, 0, 0.35); --shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.25); - --accent: orangered; - --accent-glow: rgba(255, 69, 0, 0.35); + --accent: #ff4500; + --accent-glow: color-mix(in srgb, var(--accent) 35%, transparent); + --bg-accent: color-mix(in srgb, var(--accent) 6%, transparent); --canvas-bg: #fff; --canvas-border: rgba(255, 255, 255, 0.15); background-color: var(--bg); background-image: radial-gradient(ellipse 80% 50% at 50% -10%, var(--bg-accent), transparent), - radial-gradient(ellipse 60% 40% at 100% 100%, rgba(255, 69, 0, 0.04), transparent); + radial-gradient(ellipse 60% 40% at 100% 100%, color-mix(in srgb, var(--accent) 4%, transparent), transparent); } html.light { --bg: #f4f4f6; - --bg-accent: rgba(255, 69, 0, 0.08); + --accent: #ff4500; + --accent-glow: color-mix(in srgb, var(--accent) 25%, transparent); + --bg-accent: color-mix(in srgb, var(--accent) 8%, transparent); --surface: rgba(255, 255, 255, 0.78); --surface-hover: rgba(255, 255, 255, 0.95); --surface-solid: rgba(255, 255, 255, 0.92); @@ -43,15 +45,13 @@ html.light { --placeholder: rgba(0, 0, 0, 0.35); --shadow: 0 8px 32px rgba(0, 0, 0, 0.1); --shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.06); - --accent: orangered; - --accent-glow: rgba(255, 69, 0, 0.25); --canvas-bg: #fff; --canvas-border: rgba(0, 0, 0, 0.1); background-color: var(--bg); background-image: radial-gradient(ellipse 80% 50% at 50% -10%, var(--bg-accent), transparent), - radial-gradient(ellipse 60% 40% at 0% 100%, rgba(255, 69, 0, 0.05), transparent); + radial-gradient(ellipse 60% 40% at 0% 100%, color-mix(in srgb, var(--accent) 5%, transparent), transparent); } body { @@ -186,7 +186,7 @@ textarea:focus { } textarea::selection { - background-color: orangered; + background-color: var(--accent); color: white; } @@ -301,14 +301,14 @@ button { } .btn:hover { - background-color: #ff5722; + background-color: color-mix(in srgb, var(--accent) 88%, white); transform: translateY(-1px); box-shadow: 0 6px 20px var(--accent-glow); } .btn:active { transform: translateY(0); - background-color: rgb(200, 55, 0); + background-color: color-mix(in srgb, var(--accent) 80%, black); } .btn:disabled { @@ -349,7 +349,7 @@ button { } button::selection { - background-color: rgb(172, 46, 0); + background-color: color-mix(in srgb, var(--accent) 70%, black); } .status-slot { @@ -705,7 +705,7 @@ html.light .site-banner:hover { .site-banner__brand { font-weight: 700; - color: orangered; + color: var(--accent); } .site-banner__slogan { diff --git a/src/composables/useAppConfig.js b/src/composables/useAppConfig.js new file mode 100644 index 0000000..55ab860 --- /dev/null +++ b/src/composables/useAppConfig.js @@ -0,0 +1,11 @@ +import { inject } from 'vue' + +export function useAppConfig() { + const config = inject('appConfig') + + if (!config) { + throw new Error('appConfig не найден. Проверьте инициализацию в main.js.') + } + + return config +} diff --git a/src/config/applyTheme.js b/src/config/applyTheme.js new file mode 100644 index 0000000..54cb9ff --- /dev/null +++ b/src/config/applyTheme.js @@ -0,0 +1,12 @@ +export function applyAppConfig(config) { + const root = document.documentElement + + root.style.setProperty('--accent', config.accentColor) + + const themeMeta = document.querySelector('meta[name="theme-color"]') + if (themeMeta) { + themeMeta.setAttribute('content', config.accentColor) + } + + document.title = config.title +} diff --git a/src/config/defaults.js b/src/config/defaults.js new file mode 100644 index 0000000..66143f5 --- /dev/null +++ b/src/config/defaults.js @@ -0,0 +1,7 @@ +export const defaultConfig = { + title: 'Генератор QR-кодов', + bannerBrand: 'iQR', + bannerSlogan: '— qr-код без излишеств', + bannerUrl: 'https://github.com/IliaMiheev/iQR', + accentColor: '#ff4500', +} diff --git a/src/config/loadConfig.js b/src/config/loadConfig.js new file mode 100644 index 0000000..f9c6896 --- /dev/null +++ b/src/config/loadConfig.js @@ -0,0 +1,48 @@ +import { defaultConfig } from './defaults' + +function fromBuildEnv() { + const env = import.meta.env + + return pickDefined({ + title: env.VITE_APP_TITLE, + bannerBrand: env.VITE_BANNER_BRAND, + bannerSlogan: env.VITE_BANNER_SLOGAN, + bannerUrl: env.VITE_BANNER_URL, + accentColor: env.VITE_ACCENT_COLOR, + }) +} + +function pickDefined(source) { + return Object.fromEntries( + Object.entries(source).filter(([, value]) => value !== undefined && value !== ''), + ) +} + +export async function loadConfig() { + let runtime = {} + + try { + const response = await fetch(`${import.meta.env.BASE_URL}config.json`, { + cache: 'no-store', + }) + + if (response.ok) { + runtime = await response.json() + } + } catch { + // Локально config.json может отсутствовать — это нормально. + } + + const config = { + ...defaultConfig, + ...fromBuildEnv(), + } + + for (const key of Object.keys(defaultConfig)) { + if (key in runtime) { + config[key] = runtime[key] + } + } + + return config +} diff --git a/src/main.js b/src/main.js index 9699fb0..00f0ff4 100644 --- a/src/main.js +++ b/src/main.js @@ -1,5 +1,16 @@ import { createApp } from 'vue' import App from './App.vue' import './assets/style.css' +import { loadConfig } from './config/loadConfig' +import { applyAppConfig } from './config/applyTheme' -createApp(App).mount('#app') +async function bootstrap() { + const config = await loadConfig() + applyAppConfig(config) + + const app = createApp(App) + app.provide('appConfig', config) + app.mount('#app') +} + +bootstrap()