From d475ed775b06e75796a44e5537e8ffa9d91adb84 Mon Sep 17 00:00:00 2001 From: Ilia Miheev Date: Sun, 2 Aug 2026 21:19:21 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A1=D0=B4=D0=B5=D0=BB=D0=B0=D0=BB=20=D0=BA?= =?UTF-8?q?=D0=BE=D0=BF=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5,?= =?UTF-8?q?=20=D0=BF=D0=BE=D1=87=D0=B8=D1=81=D1=82=D0=B8=D0=BB=20=D1=80?= =?UTF-8?q?=D0=B5=D0=BF=D0=BE,=20=D0=BF=D0=B5=D1=80=D0=B5=D1=88=D1=91?= =?UTF-8?q?=D0=BB=20=D0=BD=D0=B0=20=D0=B2=D0=B5=D1=80=D1=81=D0=B8=D1=8E=20?= =?UTF-8?q?1.0.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 57 ++++--------- docker-compose.yml | 2 +- package.json | 2 +- public/Безымянный.png | Bin 9249 -> 0 bytes src/components/GeneratorActions.vue | 48 +++++++++++ src/components/generators/CoinGenerator.vue | 31 ++++++- src/components/generators/ListGenerator.vue | 27 +++++- src/components/generators/RangeGenerator.vue | 31 ++++++- src/config/applyBrandingMeta.js | 83 +++++++++++++++++++ src/i18n/locales/en.js | 9 +- src/i18n/locales/ru.js | 9 +- src/main.js | 3 +- src/utils/copyResult.js | 32 +++++++ src/views/GeneratorView.vue | 6 +- vite.config.mjs | 1 + 15 files changed, 277 insertions(+), 64 deletions(-) delete mode 100644 public/Безымянный.png create mode 100644 src/components/GeneratorActions.vue create mode 100644 src/config/applyBrandingMeta.js create mode 100644 src/utils/copyResult.js diff --git a/README.md b/README.md index 3b97ddf..8c8d90c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,10 @@ # irandom +Сервис генерации случайных значений: числа из диапазона, выбор из списка, орёл/решка. +Vue 3 + Vite + Element Plus. Тема и брендинг настраиваются через env / Docker. + +**Версия:** `1.0.0` + ## Setup ``` npm install @@ -9,18 +14,10 @@ npm install ``` npm run serve ``` -or -``` -npm run dev -``` ### Production build ``` npm run build -``` - -### Preview production build -``` npm run preview ``` @@ -29,43 +26,17 @@ npm run preview npm run lint ``` -### Customization -Copy `.env.example` to `.env` and set branding / theme variables. -`npm run serve` / `npm run build` regenerates `public/config.js` automatically. - -### Docker -Build and run (config via environment, same keys as `.env.example`): +## Customization +Copy `.env.example` to `.env` and set `IRANDOM_*` variables +(name, title, logo URL, colors, default locale). +`npm run serve` / `npm run build` regenerates `public/config.js`. +## Docker ``` docker compose up --build ``` +→ http://localhost:8080 -Open http://localhost:8080 - -Change branding without rebuilding the image — only restart with new env: - -``` -docker compose up -d -``` - -Or: - -``` -docker build -t irandom . -docker run --rm -p 8080:80 ^ - -e IRANDOM_NAME=LuckyPenguin ^ - -e IRANDOM_COLOR_PRIMARY=#3DDC97 ^ - irandom -``` - -### Offline (PWA) -Production build registers a service worker and caches the app shell. -After opening the site once online: - -``` -npm run build -npm run preview -``` - -you can reload without network — generators keep working. -`config.js` uses NetworkFirst (fresh when online, cached when offline). +## Offline (PWA) +After one online visit to a production build (`build` + `preview` or Docker), +the app shell works offline. Runtime branding updates title, theme-color, icons and manifest. diff --git a/docker-compose.yml b/docker-compose.yml index 08182f3..5a0cfb4 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ services: irandom: build: . - image: irandom:latest + image: irandom:1.0.0 ports: - "8080:80" environment: diff --git a/package.json b/package.json index a980860..0b9ccde 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "irandom", - "version": "0.1.0", + "version": "1.0.0", "private": true, "scripts": { "generate-config": "node scripts/generate-config.js", diff --git a/public/Безымянный.png b/public/Безымянный.png deleted file mode 100644 index f055432c0f359287fd003c6077f02db7b83b1bfe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 9249 zcmeAS@N?(olHy`uVBq!ia0y~yV7be{z<7p(2`G}+k44ofy`glX(f`u%tWs zIx;Y9?C1WI$O`0h7I;J!GcfQS24TkI`72Tw7?ihpx;TbZ#J#<0+ z=ZH)W3TE>6o4Wa%?Xh20F7Zvt32qk{0+(F=|5YAn(vQA!O&}HjNe^i51MhoFft;F? zyG0ln8uTlt1KsmM^*zvi4EvtM0-bwc`W|mapr%Qqng%)`Hhhj}II!PSOlrr>$voCK z9T*y(9!vn@|< zn9RH8|DX5&f9CvT`8xlCA^S9X|t zOe$FvADQtW&Clpf3Ujv2)p=U;7aZH`IBALS-%~KlA|9{&7V?wD^d0XcmANm%VNN|D z<-c|LDfJbF`4d!Ly6oR{;lSBBDK}2CU8`7S8*1m+TivYUss3v!%>5gV1;1VSi8Jet zw2G(I#q~NS1;*-t2=cv<1Lwz*CK(jxtZ2D9E@hf0viGL5z ziVpu0?#QWl{@RVmv?t_yZUZx5{dPq5{@Gku4a}A?ON#*ZRBQto3F~MG1oWpa{za+`o@VtLwelK?mu-M<&;|tHl zPxz0$1-9t+M}J#}C{%v9zC|KZ$6h z{5iP8)??C_^3(U=Z6@qRt3e(=uxP!z$sAF%)*G$k1B!h$vV@CmIHsR9AKpD4t?#H- z-;Fk8my9-_M!OWA>cg#b=UCSR9t1@4m*c;aJFI|(^7YeCkP2lY#W0OKD7Ak@EqsJ? zw1+x6Rxo#Tz+-gO#%i<+HDj@acT&+Y&JtmjJT VkbKrw5DN?i22WQ%mvv4FO#m)b*<=6! diff --git a/src/components/GeneratorActions.vue b/src/components/GeneratorActions.vue new file mode 100644 index 0000000..f3a352f --- /dev/null +++ b/src/components/GeneratorActions.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/src/components/generators/CoinGenerator.vue b/src/components/generators/CoinGenerator.vue index 6f90906..09189df 100644 --- a/src/components/generators/CoinGenerator.vue +++ b/src/components/generators/CoinGenerator.vue @@ -4,9 +4,7 @@

{{ t('coin.hint') }}

- - {{ t('coin.generate') }} - + @@ -15,6 +13,8 @@ import { ElMessage } from 'element-plus' import { useI18n } from 'vue-i18n' import { flipCoin } from '@/utils/random' import { loadFormState, saveFormState } from '@/utils/persistForm' +import { copyText, formatResultForCopy } from '@/utils/copyResult' +import GeneratorActions from '@/components/GeneratorActions.vue' const STORAGE_KEY = 'coin' const DEFAULTS = { @@ -23,6 +23,13 @@ const DEFAULTS = { export default { name: 'CoinGenerator', + components: { GeneratorActions }, + props: { + result: { + type: [Number, String, Array], + default: null + } + }, emits: ['result'], setup() { const { t } = useI18n() @@ -31,6 +38,14 @@ export default { data() { return loadFormState(STORAGE_KEY, DEFAULTS) }, + computed: { + hasResult() { + if (this.result === null || this.result === undefined || this.result === '') { + return false + } + return Array.isArray(this.result) ? this.result.length > 0 : true + } + }, watch: { count: 'persist' }, @@ -47,6 +62,14 @@ export default { } const flips = flipCoin(this.count).map((side) => this.t(`coin.${side}`)) this.$emit('result', this.count === 1 ? flips[0] : flips) + }, + async onCopy() { + try { + await copyText(formatResultForCopy(this.result)) + ElMessage.success(this.t('common.copied')) + } catch { + ElMessage.error(this.t('common.copyFailed')) + } } } } @@ -54,7 +77,7 @@ export default {