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()