├── .dockerignore ├── .github └── workflows │ ├── build-dev-image.yml │ ├── build-image.yml │ └── run-checks.yml ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── art ├── db.png ├── hero.png ├── herov4.png ├── import.png ├── landing2.png ├── landing3.png ├── logo.png ├── logo_dark.png ├── organize.png ├── v3-landing.png └── your-shelf.png ├── crowdin.yml ├── docker ├── config.js ├── entrypoint.sh └── nginx.conf ├── env.d.ts ├── eslint.config.js ├── index.html ├── package-lock.json ├── package.json ├── public ├── 404.png ├── favicon.png ├── fonts │ ├── inter400.woff2 │ ├── inter700.woff2 │ ├── mono400.woff2 │ ├── noticia400.woff2 │ └── noticia700.woff2 ├── icons │ ├── maskable_icon.png │ ├── maskable_icon_x128.png │ ├── maskable_icon_x48.png │ ├── maskable_icon_x512.png │ ├── pwa256.png │ └── pwa512.png ├── logo-black.png ├── no-cocktail.jpg ├── no-ingredient.png └── robots.txt ├── src ├── App.vue ├── AppState.ts ├── AuthLayout.vue ├── PrintLayout.vue ├── api │ ├── BarAssistantClient.ts │ ├── SearchResults.d.ts │ └── api.d.ts ├── assets │ ├── alerts.css │ ├── base.css │ ├── blocks.css │ ├── buttons.css │ ├── chips.css │ ├── dialog.css │ ├── dropdown.css │ ├── fonts.css │ ├── forms.css │ ├── grid.css │ ├── main.css │ ├── search.css │ ├── table.css │ └── tags.css ├── components │ ├── AmountInput.vue │ ├── Auth │ │ ├── AuthConfirmation.vue │ │ ├── AuthForgotPassword.vue │ │ ├── AuthLogin.vue │ │ ├── AuthRegister.vue │ │ ├── AuthResetPassword.vue │ │ ├── OauthCallback.vue │ │ └── ProviderList.vue │ ├── Bar │ │ ├── BarForm.vue │ │ ├── BarIndex.vue │ │ └── BarJoinDialog.vue │ ├── Calculator │ │ ├── CalculatorForm.vue │ │ ├── CalculatorImportDialog.vue │ │ ├── CalculatorRender.vue │ │ ├── CalculatorRenderBlock.vue │ │ ├── CalculatorsIndex.vue │ │ ├── CocktailPriceCalculator.vue │ │ └── CocktailQuantityCalculator.vue │ ├── CloseButton.vue │ ├── Cocktail │ │ ├── CocktailDetails.vue │ │ ├── CocktailForm.vue │ │ ├── CocktailGridContainer.vue │ │ ├── CocktailGridItem.vue │ │ ├── CocktailImport.vue │ │ ├── CocktailIndex.vue │ │ ├── CocktailIngredient.vue │ │ ├── CocktailIngredientModal.vue │ │ ├── CocktailIngredientShare.vue │ │ ├── CocktailPrice.vue │ │ ├── CocktailPublicDetails.vue │ │ ├── CocktailRating.vue │ │ ├── CocktailThumb.vue │ │ ├── GenerateImageDialog.vue │ │ ├── PublicLinkDialog.vue │ │ ├── PublicRecipe.vue │ │ ├── SimilarCocktails.vue │ │ └── SubstituteModal.vue │ ├── CocktailFinder.vue │ ├── Collections │ │ ├── CollectionDetailsWidget.vue │ │ ├── CollectionDialog.vue │ │ ├── CollectionForm.vue │ │ ├── CollectionIndex.vue │ │ └── CollectionWidget.vue │ ├── DateFormatter.vue │ ├── Dialog │ │ ├── ConfirmDialog.vue │ │ ├── SaltRimDialog.vue │ │ └── plugin.js │ ├── EmptyState.vue │ ├── Feeds │ │ ├── FeedsIndex.vue │ │ └── FeedsRecipe.vue │ ├── GlassSelector.vue │ ├── Icons │ │ ├── IconBarShelf.vue │ │ ├── IconCalculator.vue │ │ ├── IconCheck.vue │ │ ├── IconClose.vue │ │ ├── IconCocktail.vue │ │ ├── IconExternal.vue │ │ ├── IconFavorite.vue │ │ ├── IconJigger.vue │ │ ├── IconMedal.vue │ │ ├── IconMore.vue │ │ ├── IconPublicLink.vue │ │ ├── IconRecommender.vue │ │ ├── IconShoppingCart.vue │ │ ├── IconStar.vue │ │ └── IconUserShelf.vue │ ├── ImageEditor.vue │ ├── ImageThumb.vue │ ├── ImageUpload.vue │ ├── Ingredient │ │ ├── Hierarchy │ │ │ ├── IngredientTreeNode.vue │ │ │ └── IngredientTreeNodeItem.vue │ │ ├── IngredientDetails.vue │ │ ├── IngredientForm.vue │ │ ├── IngredientGridContainer.vue │ │ ├── IngredientGridItem.vue │ │ ├── IngredientHierarchy.vue │ │ ├── IngredientImage.vue │ │ ├── IngredientImport.vue │ │ ├── IngredientIndex.vue │ │ ├── IngredientListContainer.vue │ │ ├── IngredientSpotlight.vue │ │ └── RecommendedIngredients.vue │ ├── IngredientFinder.vue │ ├── IngredientFinderBasic.vue │ ├── Layout │ │ ├── SiteFooter.vue │ │ ├── SiteHeader.vue │ │ └── SiteLogo.vue │ ├── ListItemContainer.vue │ ├── Menu │ │ ├── MenuIndex.vue │ │ └── MenuPublicIndex.vue │ ├── MiniRating.vue │ ├── Note │ │ ├── NoteDetails.vue │ │ └── NoteDialog.vue │ ├── OverlayLoader.vue │ ├── PageHeader.vue │ ├── Print │ │ ├── PrintCocktail.vue │ │ └── PrintShoppingList.vue │ ├── RatingActions.vue │ ├── SaltRimCheckbox.vue │ ├── SaltRimDropdown.vue │ ├── SaltRimRadio.vue │ ├── SaltRimSpinner.vue │ ├── Search │ │ ├── FilterIngredientsModal.vue │ │ ├── OffCanvas.vue │ │ ├── SearchPagination.vue │ │ └── SearchRefinement.vue │ ├── Settings │ │ ├── APIForm.vue │ │ ├── APIList.vue │ │ ├── BillingInfo.vue │ │ ├── CocktailMethodsForm.vue │ │ ├── CocktailMethodsList.vue │ │ ├── ExportForm.vue │ │ ├── ExportsList.vue │ │ ├── GlassForm.vue │ │ ├── GlassesList.vue │ │ ├── PriceCategoriesList.vue │ │ ├── PriceCategoryForm.vue │ │ ├── ProfileForm.vue │ │ ├── SettingsNavigation.vue │ │ ├── TagForm.vue │ │ ├── TagsList.vue │ │ ├── UserForm.vue │ │ ├── UsersList.vue │ │ ├── UtensilForm.vue │ │ └── UtensilsList.vue │ ├── Shelf │ │ └── ShelfIndex.vue │ ├── ShoppingList │ │ └── ShoppingListIndex.vue │ ├── SiteAutocomplete.vue │ ├── SourcePresenter.vue │ ├── StatusCheck.vue │ ├── SubscriptionCheck.vue │ ├── TagSelector.vue │ ├── ThemeToggle.vue │ ├── TimeStamps.vue │ ├── ToggleIngredientBarShelf.vue │ ├── ToggleIngredientShelf.vue │ ├── ToggleIngredientShoppingCart.vue │ ├── Units │ │ ├── UnitConverter.vue │ │ └── UnitPicker.vue │ └── WakeLockToggle.vue ├── composables │ ├── __tests__ │ │ ├── useHtmlDecode.test.ts │ │ ├── useRecommendedAmount.test.ts │ │ └── useUnits.test.ts │ ├── confirm.ts │ ├── eventBus.ts │ ├── ingredientBg.ts │ ├── title.ts │ ├── toast.ts │ ├── useAuth.ts │ ├── useGetRoleName.ts │ ├── useHtmlDecode.ts │ ├── useRecommendedAmounts.ts │ ├── useTheme.ts │ └── useUnits.ts ├── locales │ ├── de-DE.js │ ├── en-US.js │ ├── fr-FR.js │ ├── hr-HR.js │ ├── it-IT.js │ ├── messages │ │ ├── cs-CZ.json │ │ ├── da-DK.json │ │ ├── de-DE.json │ │ ├── el-GR.json │ │ ├── en-US.json │ │ ├── es-ES.json │ │ ├── fi-FI.json │ │ ├── fr-FR.json │ │ ├── hr-HR.json │ │ ├── hu-HU.json │ │ ├── it-IT.json │ │ ├── nl-NL.json │ │ ├── no-NO.json │ │ ├── pl-PL.json │ │ ├── pt-PT.json │ │ ├── ro-RO.json │ │ ├── sv-SE.json │ │ ├── zh-CN.json │ │ └── zh-TW.json │ ├── pl-PL.js │ ├── sv-SE.js │ └── zh-CN.js ├── main.ts ├── router │ └── index.js └── views │ ├── BarFormView.vue │ ├── BarJoinView.vue │ ├── BarsView.vue │ ├── CalculatorFormView.vue │ ├── CalculatorsIndex.vue │ ├── CocktailCollections.vue │ ├── CocktailPrintView.vue │ ├── CocktailView.vue │ ├── CocktailsFormView.vue │ ├── CocktailsScrapeView.vue │ ├── CocktailsView.vue │ ├── ConfirmationView.vue │ ├── FeedsView.vue │ ├── ForgotPasswordView.vue │ ├── HomeView.vue │ ├── IngredientFormView.vue │ ├── IngredientImport.vue │ ├── IngredientView.vue │ ├── IngredientsView.vue │ ├── LoginView.vue │ ├── MenuPublicView.vue │ ├── MenuView.vue │ ├── OauthCallbackView.vue │ ├── PageNotFound.vue │ ├── ProfileView.vue │ ├── PublicCocktailView.vue │ ├── QuantityCalcView.vue │ ├── RegisterView.vue │ ├── ResetPasswordView.vue │ ├── ServiceDownView.vue │ ├── SettingsAPIView.vue │ ├── SettingsBillingView.vue │ ├── SettingsCocktailMethodsView.vue │ ├── SettingsExportsView.vue │ ├── SettingsGlassesView.vue │ ├── SettingsPriceCategoriesView.vue │ ├── SettingsProfileView.vue │ ├── SettingsTagsView.vue │ ├── SettingsUsersView.vue │ ├── SettingsUtensilsView.vue │ ├── ShelfShoppingListPrintView.vue │ └── ShoppingListView.vue ├── tsconfig.json └── vite.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | .env 3 | .env.dist 4 | README.md 5 | /art 6 | public/config.js 7 | /dist 8 | /.github 9 | /.vscode 10 | /.git 11 | 12 | Dockerfile 13 | .dockerignore 14 | crowdin.yml -------------------------------------------------------------------------------- /.github/workflows/build-dev-image.yml: -------------------------------------------------------------------------------- 1 | name: Build unstable docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'develop' 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | packages: write 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v3 18 | - 19 | name: Set up QEMU 20 | uses: docker/setup-qemu-action@v2 21 | - 22 | name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v2 24 | - 25 | name: Login to Docker Hub 26 | uses: docker/login-action@v2 27 | with: 28 | username: ${{ secrets.HUB_BASS_USERNAME }} 29 | password: ${{ secrets.HUB_BASS_TOKEN }} 30 | - 31 | name: Login to GitHub Container Registry 32 | uses: docker/login-action@v2 33 | with: 34 | registry: ghcr.io 35 | username: ${{ github.actor }} 36 | password: ${{ secrets.GITHUB_TOKEN }} 37 | - 38 | name: Build and push 39 | uses: docker/build-push-action@v3 40 | with: 41 | context: . 42 | platforms: linux/amd64,linux/arm64 43 | push: true 44 | build-args: BUILD_VERSION=develop 45 | tags: | 46 | barassistant/salt-rim:dev 47 | ghcr.io/${{ github.repository_owner }}/salt-rim:dev 48 | cache-from: type=gha 49 | cache-to: type=gha,mode=max 50 | -------------------------------------------------------------------------------- /.github/workflows/build-image.yml: -------------------------------------------------------------------------------- 1 | name: Build stable docker image 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | jobs: 9 | docker: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: read 13 | packages: write 14 | steps: 15 | - 16 | name: Checkout 17 | uses: actions/checkout@v3 18 | - 19 | name: Docker meta 20 | id: meta 21 | uses: docker/metadata-action@v5 22 | with: 23 | images: | 24 | barassistant/salt-rim 25 | ghcr.io/${{ github.repository_owner }}/salt-rim 26 | flavor: | 27 | latest=false 28 | tags: | 29 | type=semver,pattern={{version}} 30 | type=semver,pattern={{major}}.{{minor}} 31 | type=semver,pattern=v{{major}} 32 | - 33 | name: Set up QEMU 34 | uses: docker/setup-qemu-action@v2 35 | - 36 | name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v2 38 | - 39 | name: Login to Docker Hub 40 | uses: docker/login-action@v2 41 | with: 42 | username: ${{ secrets.HUB_BASS_USERNAME }} 43 | password: ${{ secrets.HUB_BASS_TOKEN }} 44 | - 45 | name: Login to GitHub Container Registry 46 | uses: docker/login-action@v2 47 | with: 48 | registry: ghcr.io 49 | username: ${{ github.actor }} 50 | password: ${{ secrets.GITHUB_TOKEN }} 51 | - 52 | name: Build and push 53 | uses: docker/build-push-action@v3 54 | with: 55 | context: . 56 | platforms: linux/amd64,linux/arm64 57 | build-args: BUILD_VERSION=${{github.ref_name}} 58 | push: true 59 | tags: ${{ steps.meta.outputs.tags }} 60 | -------------------------------------------------------------------------------- /.github/workflows/run-checks.yml: -------------------------------------------------------------------------------- 1 | name: Run Vue checks 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up Node.js 20 | uses: actions/setup-node@v3 21 | with: 22 | node-version: '22' 23 | 24 | - name: Install dependencies 25 | run: npm install 26 | 27 | - name: Run type checks 28 | run: npm run type-check 29 | 30 | - name: Run ESLint 31 | run: npm run lint 32 | 33 | - name: Run tests 34 | run: npm run test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .env 3 | dist/ 4 | public/config.js 5 | .vscode 6 | spec.yml 7 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:current-alpine3.20 AS build 2 | 3 | ARG BUILD_VERSION 4 | ENV BUILD_VERSION=${BUILD_VERSION:-develop} 5 | 6 | WORKDIR /app 7 | COPY package*.json . 8 | RUN npm install 9 | COPY . . 10 | 11 | RUN sed -i "s/{{VERSION}}/$BUILD_VERSION/g" ./docker/config.js 12 | 13 | RUN npm run build 14 | 15 | FROM nginx AS prod 16 | 17 | LABEL org.opencontainers.image.source="https://github.com/karlomikus/vue-salt-rim" 18 | 19 | COPY --from=build /app/dist /var/www/html 20 | 21 | COPY --from=build /app/docker/config.js /var/www/config.js 22 | COPY ./docker/nginx.conf /etc/nginx/nginx.conf 23 | COPY ./docker/entrypoint.sh /usr/local/bin/entrypoint 24 | 25 | RUN chmod +x /usr/local/bin/entrypoint 26 | 27 | EXPOSE 8080 28 | 29 | CMD [ "entrypoint" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Karlo Mikuš 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /art/db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/art/db.png -------------------------------------------------------------------------------- /art/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/art/hero.png -------------------------------------------------------------------------------- /art/herov4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/art/herov4.png -------------------------------------------------------------------------------- /art/import.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/art/import.png -------------------------------------------------------------------------------- /art/landing2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/art/landing2.png -------------------------------------------------------------------------------- /art/landing3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/art/landing3.png -------------------------------------------------------------------------------- /art/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/art/logo.png -------------------------------------------------------------------------------- /art/logo_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/art/logo_dark.png -------------------------------------------------------------------------------- /art/organize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/art/organize.png -------------------------------------------------------------------------------- /art/v3-landing.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/art/v3-landing.png -------------------------------------------------------------------------------- /art/your-shelf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/art/your-shelf.png -------------------------------------------------------------------------------- /crowdin.yml: -------------------------------------------------------------------------------- 1 | files: 2 | - source: /src/locales/messages/en-US.json 3 | translation: /src/locales/messages/%locale%.json 4 | -------------------------------------------------------------------------------- /docker/config.js: -------------------------------------------------------------------------------- 1 | window.srConfig = {} 2 | window.srConfig.VERSION = "{{VERSION}}" 3 | window.srConfig.API_URL = "$API_URL" 4 | window.srConfig.MEILISEARCH_URL = "$MEILISEARCH_URL"; 5 | window.srConfig.DEFAULT_LOCALE = "$DEFAULT_LOCALE"; 6 | window.srConfig.ENV = "$SALT_RIM_ENV"; 7 | window.srConfig.MAILS_ENABLED = "$MAILS_ENABLED" === 'true'; 8 | window.srConfig.BILLING_TOKEN = "$BILLING_TOKEN"; 9 | window.srConfig.BILLING_ENABLED = "$BILLING_ENABLED" === 'true'; 10 | window.srConfig.BILLING_ENV = "$BILLING_ENV"; 11 | window.srConfig.ANALYTICS_HOST = "$ANALYTICS_HOST"; 12 | window.srConfig.ALLOW_REGISTRATION = "$ALLOW_REGISTRATION"; 13 | window.srConfig.SENTRY_DSN = "$SENTRY_DSN"; 14 | window.srConfig.REDIRECT_TO_SSO = "$REDIRECT_TO_SSO" === 'true'; 15 | -------------------------------------------------------------------------------- /docker/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | envsubst < /var/www/config.js > /var/www/html/config.js 4 | 5 | exec nginx -g "daemon off;" 6 | -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | 3 | events { worker_connections 1024; } 4 | 5 | http { 6 | include mime.types; 7 | 8 | sendfile on; 9 | tcp_nopush on; 10 | tcp_nodelay on; 11 | 12 | access_log off; 13 | error_log stderr error; 14 | 15 | client_max_body_size 100M; 16 | 17 | server { 18 | listen 8080 default_server; 19 | listen [::]:8080 default_server; 20 | server_name _; 21 | root /var/www/html/; 22 | 23 | location / { 24 | try_files $uri $uri/ /index.html; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | declare module 'sortablejs' 5 | declare module '@types/slug' 6 | 7 | interface Window { 8 | srConfig: { 9 | VERSION: string, 10 | API_URL: string, 11 | MEILISEARCH_URL: string, 12 | DEFAULT_LOCALE: string, 13 | ENV: string, 14 | MAILS_ENABLED: boolean, 15 | BILLING_TOKEN: string, 16 | BILLING_ENABLED: boolean, 17 | BILLING_ENV: string, 18 | ANALYTICS_HOST: string, 19 | ALLOW_REGISTRATION: string, 20 | SENTRY_DSN: string, 21 | REDIRECT_TO_SSO: boolean, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import pluginVue from 'eslint-plugin-vue' 2 | import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript' 3 | import pluginVitest from '@vitest/eslint-plugin' 4 | 5 | export default defineConfigWithVueTs( 6 | { 7 | name: 'app/files-to-lint', 8 | files: ['**/*.{ts,mts,tsx,vue}'], 9 | }, 10 | 11 | { 12 | name: 'app/files-to-ignore', 13 | ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'], 14 | }, 15 | 16 | pluginVue.configs['flat/base'], 17 | vueTsConfigs.recommended, 18 | 19 | { 20 | rules: { 21 | "vue/html-indent": ["warn", 4], 22 | "vue/max-attributes-per-line": ['off'], 23 | "vue/block-lang": ['off'], 24 | "vue/multiline-html-element-content-newline": ['off'], 25 | "vue/html-self-closing": ['off'], 26 | "vue/no-v-html": ['off'], 27 | "vue/singleline-html-element-content-newline": ['off'], 28 | "vue/v-on-event-hyphenation": ['warn', 'always', { 29 | autofix: true 30 | }], 31 | "@typescript-eslint/no-explicit-any": ['off'], 32 | "@typescript-eslint/no-unused-vars": ['off'], 33 | "no-unused-expressions": "off", 34 | "@typescript-eslint/no-unused-expressions": "off", 35 | "@typescript-eslint/ban-ts-comment": "off", 36 | "@typescript-eslint/no-this-alias": "off", 37 | } 38 | }, 39 | 40 | { 41 | ...pluginVitest.configs.recommended, 42 | files: ['src/**/__tests__/*'], 43 | }, 44 | ) 45 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Bar Assistant 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-salt-rim", 3 | "type": "module", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview --port 4173", 8 | "lint": "eslint src/", 9 | "lint-fix": "eslint --fix src", 10 | "test": "vitest", 11 | "type-check": "vue-tsc --build", 12 | "gen": "npx openapi-typescript ./spec.yml -o ./src/api/api.d.ts" 13 | }, 14 | "dependencies": { 15 | "@floating-ui/vue": "^1.1.5", 16 | "@meilisearch/instant-meilisearch": "^0.25.0", 17 | "@paddle/paddle-js": "^1.2.1", 18 | "@sentry/vue": "^8.47.0", 19 | "@types/sortablejs": "^1.15.8", 20 | "@vueuse/core": "^11.0.3", 21 | "cropperjs": "^1.6.1", 22 | "dayjs": "^1.11.7", 23 | "entities": "^6.0.0", 24 | "format-quantity": "^3.0.0", 25 | "html-to-image": "^1.11.11", 26 | "mathjs": "^14.0.1", 27 | "micromark": "^4.0.0", 28 | "numeric-quantity": "^2.0.1", 29 | "openapi-fetch": "^0.11.1", 30 | "plausible-tracker": "^0.3.8", 31 | "qrcode-vue3": "^1.6.8", 32 | "remove-markdown": "^0.5.0", 33 | "slug": "^8.2.3", 34 | "sortablejs": "^1.15.0", 35 | "swiper": "^11.0.0", 36 | "thumbhash": "^0.1.1", 37 | "treeflex": "^2.0.1", 38 | "vue": "^3.2.40", 39 | "vue-i18n": "^10.0.0", 40 | "vue-instantsearch": "^4.6.0", 41 | "vue-router": "^4.1.5", 42 | "vue-toast-notification": "^3.0.4" 43 | }, 44 | "devDependencies": { 45 | "@types/slug": "^5.0.9", 46 | "@vitejs/plugin-vue": "^5.2.1", 47 | "@vitest/eslint-plugin": "1.1.31", 48 | "@vue/eslint-config-typescript": "^14.4.0", 49 | "@vue/tsconfig": "^0.7.0", 50 | "eslint": "^9.22.0", 51 | "eslint-plugin-vue": "^10.0", 52 | "openapi-typescript": "^7.3.0", 53 | "typescript": "^5.5.4", 54 | "vite": "^6.2.3", 55 | "vite-plugin-pwa": "^0.21.1", 56 | "vite-plugin-vue-devtools": "^7.7.2", 57 | "vitest": "^3.0.8", 58 | "vue-tsc": "^2.2.2" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /public/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/404.png -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/favicon.png -------------------------------------------------------------------------------- /public/fonts/inter400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/fonts/inter400.woff2 -------------------------------------------------------------------------------- /public/fonts/inter700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/fonts/inter700.woff2 -------------------------------------------------------------------------------- /public/fonts/mono400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/fonts/mono400.woff2 -------------------------------------------------------------------------------- /public/fonts/noticia400.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/fonts/noticia400.woff2 -------------------------------------------------------------------------------- /public/fonts/noticia700.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/fonts/noticia700.woff2 -------------------------------------------------------------------------------- /public/icons/maskable_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/icons/maskable_icon.png -------------------------------------------------------------------------------- /public/icons/maskable_icon_x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/icons/maskable_icon_x128.png -------------------------------------------------------------------------------- /public/icons/maskable_icon_x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/icons/maskable_icon_x48.png -------------------------------------------------------------------------------- /public/icons/maskable_icon_x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/icons/maskable_icon_x512.png -------------------------------------------------------------------------------- /public/icons/pwa256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/icons/pwa256.png -------------------------------------------------------------------------------- /public/icons/pwa512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/icons/pwa512.png -------------------------------------------------------------------------------- /public/logo-black.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/logo-black.png -------------------------------------------------------------------------------- /public/no-cocktail.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/no-cocktail.jpg -------------------------------------------------------------------------------- /public/no-ingredient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/karlomikus/vue-salt-rim/6da49322f568357f4b9142be1de356620d6d2db3/public/no-ingredient.png -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Allow: / 3 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | -------------------------------------------------------------------------------- /src/AuthLayout.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/PrintLayout.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /src/api/SearchResults.d.ts: -------------------------------------------------------------------------------- 1 | export interface SearchResults { 2 | ingredient: { 3 | id: number; 4 | slug: string; 5 | name: string; 6 | image_url: string|null; 7 | description: string|null; 8 | category: string|null; 9 | bar_id: number; 10 | units?: string|null; 11 | }, 12 | cocktail: { 13 | id: number; 14 | name: string; 15 | slug: string; 16 | description: string|null; 17 | image_url: string|null; 18 | short_ingredients: string[]; 19 | tags: string[]; 20 | bar_id: number; 21 | } 22 | } -------------------------------------------------------------------------------- /src/assets/alerts.css: -------------------------------------------------------------------------------- 1 | .alert { 2 | --_clr-text: #000; 3 | --_clr-border: #000; 4 | --_clr-bg: #fff; 5 | background: var(--_clr-bg); 6 | border: 2px solid var(--_clr-border); 7 | padding: 1rem; 8 | border-radius: var(--radius-1); 9 | color: var(--_clr-text); 10 | } 11 | 12 | .alert h3 { 13 | font-weight: var(--fw-bold); 14 | font-size: 0.7rem; 15 | letter-spacing: 1px; 16 | text-transform: uppercase; 17 | margin-bottom: 0.3rem; 18 | } 19 | 20 | .alert.alert--info { 21 | --_clr-text: rgb(64, 75, 112); 22 | --_clr-border: rgb(64, 75, 112); 23 | --_clr-bg: rgb(247, 249, 255); 24 | border-color: var(--_clr-border); 25 | } 26 | 27 | .dark-theme .alert.alert--info { 28 | --_clr-text: #a8c6f3; 29 | --_clr-border: #2c4b79; 30 | --_clr-bg: #0e213d; 31 | } 32 | 33 | .alert.alert--warning { 34 | --_clr-text: rgb(173, 68, 7); 35 | --_clr-border: rgb(173, 68, 7); 36 | --_clr-bg: rgb(255, 250, 247); 37 | border-color: var(--_clr-border); 38 | } -------------------------------------------------------------------------------- /src/assets/blocks.css: -------------------------------------------------------------------------------- 1 | .block-container { 2 | --_bc-padding: 1.5rem; 3 | background: #fcf9fb; 4 | border-bottom: 1px solid var(--clr-accent-200); 5 | border-top: 2px solid #fff; 6 | border-radius: var(--radius-2); 7 | box-shadow: var(--shadow-elevation-medium); 8 | } 9 | 10 | @media (max-width: 450px) { 11 | .block-container { 12 | --_bc-padding: 1rem; 13 | } 14 | } 15 | 16 | .dark-theme .block-container { 17 | background: var(--clr-dark-main-800); 18 | border-top: 1px solid var(--clr-dark-main-650); 19 | border-bottom: 1px solid var(--clr-dark-main-900); 20 | box-shadow: var(--shadow-elevation-medium-dark); 21 | } 22 | 23 | .block-container.block-container--padded { 24 | padding: var(--_bc-padding); 25 | } 26 | 27 | .block-container.block-container--hover:hover { 28 | background: rgba(255, 255, 255, .8); 29 | /* border-bottom-color: var(--clr-link-color-hover); */ 30 | } 31 | 32 | .dark-theme .block-container.block-container--hover:hover { 33 | background: var(--clr-dark-main-700); 34 | border-top-color: var(--clr-dark-main-600); 35 | /* border-bottom-color: var(--clr-dark-main-800); */ 36 | } 37 | 38 | .details-block-container__title { 39 | font-family: var(--font-heading); 40 | font-size: 1.5rem; 41 | font-weight: var(--fw-bold); 42 | margin-bottom: 1rem; 43 | line-height: 1; 44 | color: var(--clr-accent-800); 45 | } 46 | 47 | .dark-theme .details-block-container__title { 48 | color: var(--clr-accent-200); 49 | } 50 | 51 | .block-container.block-container--inset { 52 | border: 0; 53 | background-color: #f4edf2; 54 | border-bottom: 1px solid #fff; 55 | border-radius: var(--radius-2); 56 | box-shadow: 57 | inset 0px 0.4px 0.5px hsl(var(--shadow-color) / 0.25), 58 | inset 0px 1.1px 1.2px -0.8px hsl(var(--shadow-color) / 0.25), 59 | inset 0px 2.6px 2.9px -1.7px hsl(var(--shadow-color) / 0.25), 60 | inset 0px 6.3px 7.1px -2.5px hsl(var(--shadow-color) / 0.25); 61 | } 62 | 63 | .block-container.block-container--inset.block-container--padded { 64 | padding: var(--gap-size-3); 65 | } 66 | 67 | .dark-theme .block-container.block-container--inset { 68 | background-color: rgba(0, 0, 0, .15); 69 | border-bottom: 1px solid rgba(255, 255, 255, .1); 70 | box-shadow: 71 | inset 0px 0.4px 0.5px hsl(var(--shadow-color-dark) / 0.25), 72 | inset 0px 1.1px 1.2px -0.8px hsl(var(--shadow-color-dark) / 0.25), 73 | inset 0px 2.6px 2.9px -1.7px hsl(var(--shadow-color-dark) / 0.25), 74 | inset 0px 6.3px 7.1px -2.5px hsl(var(--shadow-color-dark) / 0.25); 75 | } 76 | 77 | .block-container.block-container--placeholder { 78 | background: #d9e3f6; 79 | border-top-color: #f5f9ff; 80 | border-bottom-color: #a7badd; 81 | } 82 | 83 | .dark-theme .block-container.block-container--placeholder { 84 | background: #ec8703; 85 | border-top-color: #ffcc8a; 86 | border-bottom-color: #4b2a00; 87 | } -------------------------------------------------------------------------------- /src/assets/chips.css: -------------------------------------------------------------------------------- 1 | .item-details__chips { 2 | display: flex; 3 | flex-wrap: wrap; 4 | gap: var(--gap-size-2); 5 | margin-bottom: 1rem; 6 | } 7 | 8 | .item-details__chips__group__title { 9 | font-size: 0.65rem; 10 | margin-bottom: -2px; 11 | color: var(--clr-gray-600); 12 | } 13 | 14 | .dark-theme .item-details__chips__group__title { 15 | color: rgba(255, 255, 255, .4); 16 | } 17 | 18 | .item-details__chips .rating { 19 | line-height: 1; 20 | } 21 | 22 | .chips-list { 23 | list-style: none; 24 | padding: 0; 25 | margin: 0; 26 | display: flex; 27 | flex-wrap: wrap; 28 | gap: var(--gap-size-2); 29 | } 30 | 31 | .chip { 32 | display: block; 33 | padding: 1px 9px; 34 | font-size: 0.75rem; 35 | background-color: #E6DBF0; 36 | color: #3a304d; 37 | border-radius: var(--radius-1); 38 | text-decoration: none; 39 | } 40 | 41 | a.chip:hover, 42 | a.chip:active, 43 | a.chip:focus { 44 | background-color: #c1cef0; 45 | color: #1e273b; 46 | } 47 | 48 | .dark-theme .chip { 49 | background-color: #572d2c; 50 | color: #f3dada; 51 | } 52 | 53 | .dark-theme a.chip:hover, 54 | .dark-theme a.chip:active, 55 | .dark-theme a.chip:focus { 56 | background-color: #743f3e; 57 | color: #fff0f0; 58 | } 59 | -------------------------------------------------------------------------------- /src/assets/dialog.css: -------------------------------------------------------------------------------- 1 | .dialog { 2 | --dialog-margin: 2rem; 3 | --dialog-padding: 1.5rem; 4 | inset: 0; 5 | position: fixed; 6 | transition: opacity 250ms ease, backdrop-filter 250ms ease; 7 | z-index: var(--z-dialog); 8 | } 9 | 10 | @media (max-width: 450px) { 11 | .dialog { 12 | --dialog-margin: 0.5rem; 13 | --dialog-padding: 1rem; 14 | } 15 | } 16 | 17 | .dialog__container { 18 | height: 100%; 19 | outline: 0; 20 | overflow-x: hidden; 21 | overflow-y: auto; 22 | padding: var(--dialog-margin); 23 | display: flex; 24 | align-items: center; 25 | justify-content: center; 26 | } 27 | 28 | .dialog-animation-enter-from, 29 | .dialog-animation-leave-to { 30 | opacity: 0; 31 | backdrop-filter: blur(0) 32 | } 33 | 34 | .dialog__overlay { 35 | background-color: rgba(180, 175, 189, 0.8); 36 | position: fixed; 37 | inset: 0; 38 | pointer-events: auto; 39 | backdrop-filter: blur(5px); 40 | } 41 | 42 | .dark-theme .dialog__overlay { 43 | background-color: rgba(11, 5, 21, 0.8); 44 | } 45 | 46 | .dialog__content { 47 | outline: none; 48 | pointer-events: auto; 49 | contain: layout; 50 | background-color: #fcf9fb; 51 | border-top: 2px solid #fff; 52 | padding: var(--dialog-padding); 53 | margin: auto; 54 | border-radius: var(--radius-3); 55 | transition: all 200ms cubic-bezier(0.16, 1, 0.3, 1); 56 | max-width: 600px; 57 | width: 100%; 58 | box-shadow: var(--shadow-elevation-high-dark); 59 | } 60 | 61 | .dark-theme .dialog__content { 62 | background-color: var(--clr-dark-main-800); 63 | border-bottom: 1px solid var(--clr-dark-main-900); 64 | border-top: 1px solid var(--clr-dark-main-650); 65 | } 66 | 67 | .dialog-animation-enter-from .dialog__content, 68 | .dialog-animation-leave-to .dialog__content { 69 | transform: translate(0, 2rem); 70 | opacity: 0; 71 | } 72 | 73 | .dialog-title { 74 | font-size: 1.5em; 75 | margin-bottom: 1.5rem; 76 | line-height: 1; 77 | } 78 | 79 | @media (max-width: 450px) { 80 | .dialog-title { 81 | margin-bottom: 1rem; 82 | margin-right: 1rem; 83 | } 84 | } 85 | 86 | .dialog-actions { 87 | display: flex; 88 | justify-content: end; 89 | flex-wrap: wrap; 90 | gap: var(--gap-size-2); 91 | margin-top: 1.5rem; 92 | } 93 | -------------------------------------------------------------------------------- /src/assets/dropdown.css: -------------------------------------------------------------------------------- 1 | .floating-element { 2 | width: max-content; 3 | position: absolute; 4 | top: 0; 5 | left: 0; 6 | z-index: var(--z-dropdown); 7 | } 8 | 9 | .dropdown-menu { 10 | background-color: #fff; 11 | padding: 0.5rem; 12 | border-radius: var(--radius-3); 13 | display: flex; 14 | flex-direction: column; 15 | max-width: 270px; 16 | animation: slide-in-top 400ms cubic-bezier(0.23, 1, 0.32, 1) both; 17 | box-shadow: var(--shadow-elevation-low); 18 | } 19 | 20 | .dropdown-menu .dropdown-menu__item { 21 | padding: 6px 16px 6px 12px; 22 | display: flex; 23 | align-items: center; 24 | color: black; 25 | font-size: 0.9rem; 26 | border-radius: var(--radius-2); 27 | text-decoration: none; 28 | white-space: nowrap; 29 | } 30 | 31 | .dropdown-menu .dropdown-menu__item svg { 32 | margin-right: 12px; 33 | flex-shrink: 0; 34 | /* width: 20px; */ 35 | } 36 | 37 | .dropdown-menu .dropdown-menu__item:hover { 38 | color: #fff; 39 | background-color: var(--clr-gray-800); 40 | } 41 | 42 | .dropdown-menu .dropdown-menu__item:hover svg { 43 | fill: #fff; 44 | } 45 | 46 | .dropdown-menu .dropdown-menu__title { 47 | font-size: 0.8rem; 48 | padding: 3px 16px 3px 12px; 49 | font-weight: bold; 50 | color: var(--clr-gray-400); 51 | } 52 | 53 | .dropdown-menu__separator { 54 | border: 0; 55 | height: 1px; 56 | background-color: var(--clr-gray-200); 57 | margin: 0.5rem; 58 | } 59 | 60 | @keyframes slide-in-top { 61 | 0% { 62 | top: -10px; 63 | opacity: 0; 64 | } 65 | 66 | 100% { 67 | top: 0; 68 | opacity: 1; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/assets/grid.css: -------------------------------------------------------------------------------- 1 | .sr-grid { 2 | display: grid; 3 | gap: var(--gap-size-2); 4 | } 5 | 6 | .sr-grid.sr-grid--2-col { 7 | grid-template-columns: 1fr 1fr; 8 | } 9 | 10 | .sr-grid.sr-grid--3-col { 11 | grid-template-columns: 1fr 1fr 1fr; 12 | } 13 | 14 | @media (max-width: 350px) { 15 | .sr-grid.sr-grid--2-col { 16 | grid-template-columns: 1fr; 17 | } 18 | } 19 | 20 | @media (max-width: 450px) { 21 | .sr-grid.sr-grid--3-col { 22 | grid-template-columns: 1fr; 23 | } 24 | } -------------------------------------------------------------------------------- /src/assets/table.css: -------------------------------------------------------------------------------- 1 | .table { 2 | --table-border-width: 1px; 3 | --table-border-color: var(--clr-accent-100); 4 | --table-row-hover-clr: var(--clr-accent-50); 5 | --table-small-clr: var(--clr-gray-500); 6 | width: 100%; 7 | caption-side: bottom; 8 | border-collapse: collapse; 9 | border-spacing: 0; 10 | text-align: left; 11 | } 12 | 13 | .dark-theme .table { 14 | --table-row-hover-clr: var(--clr-dark-main-900); 15 | --table-border-color: var(--clr-dark-main-700); 16 | --table-small-clr: var(--clr-dark-main-400); 17 | } 18 | 19 | .table td { 20 | padding: .5rem; 21 | box-shadow: inset var(--table-border-width) calc(var(--table-border-width) * -1) 0 0 var(--table-border-color); 22 | } 23 | 24 | .table tr:last-child td { 25 | box-shadow: inset var(--table-border-width) 0 0 0 var(--table-border-color); 26 | } 27 | 28 | .table td:first-child { 29 | box-shadow: inset 0 calc(var(--table-border-width) * -1) 0 0 var(--table-border-color); 30 | } 31 | 32 | .table td:first-child { 33 | box-shadow: inset 0 calc(var(--table-border-width) * -1) 0 0 var(--table-border-color); 34 | } 35 | 36 | .table tr:last-child td:first-child { 37 | box-shadow: none; 38 | } 39 | 40 | .table th { 41 | font-weight: var(--fw-bold); 42 | font-size: 0.85rem; 43 | padding: .5rem; 44 | border-bottom: 2px solid var(--clr-accent-200); 45 | } 46 | 47 | .table tr:hover td { 48 | background-color: var(--table-row-hover-clr); 49 | } 50 | 51 | .table td small { 52 | color: var(--table-small-clr); 53 | line-height: 1.4; 54 | display: block; 55 | } 56 | -------------------------------------------------------------------------------- /src/assets/tags.css: -------------------------------------------------------------------------------- 1 | .tag-container { 2 | display: flex; 3 | flex-wrap: wrap; 4 | gap: var(--gap-size-1); 5 | } 6 | 7 | .tag { 8 | --color-tag: #DFE7FD; 9 | --color-text: var(--clr-gray-800); 10 | 11 | text-decoration: none; 12 | background-color: none; 13 | padding: 2px 6px; 14 | border-radius: var(--radius-2); 15 | font-size: 0.75rem; 16 | line-height: 1.3; 17 | white-space: nowrap; 18 | } 19 | 20 | .dark-theme .tag { 21 | --color-tag: var(--clr-gray-700); 22 | --color-text: var(--clr-gray-200); 23 | } 24 | 25 | .tag.tag--link { 26 | border: 1px solid var(--clr-accent-300); 27 | } 28 | 29 | .tag.tag--link:hover, 30 | .tag.tag--link:focus, 31 | .tag.tag--link:active { 32 | background-color: #fff; 33 | } 34 | 35 | .tag.tag--link.tag--is-selected { 36 | background-color: var(--clr-gray-800); 37 | border: 1px solid var(--clr-gray-800); 38 | color: var(--color-text); 39 | } 40 | 41 | .tag.tag--background { 42 | background-color: var(--color-tag); 43 | color: var(--color-text); 44 | } 45 | -------------------------------------------------------------------------------- /src/components/AmountInput.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 63 | 64 | -------------------------------------------------------------------------------- /src/components/Auth/AuthConfirmation.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 24 | -------------------------------------------------------------------------------- /src/components/Auth/AuthForgotPassword.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 51 | -------------------------------------------------------------------------------- /src/components/Auth/AuthRegister.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 76 | -------------------------------------------------------------------------------- /src/components/Auth/AuthResetPassword.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 66 | -------------------------------------------------------------------------------- /src/components/Auth/OauthCallback.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | -------------------------------------------------------------------------------- /src/components/Bar/BarJoinDialog.vue: -------------------------------------------------------------------------------- 1 | 21 | 52 | -------------------------------------------------------------------------------- /src/components/Calculator/CalculatorImportDialog.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 55 | -------------------------------------------------------------------------------- /src/components/Calculator/CalculatorRender.vue: -------------------------------------------------------------------------------- 1 | 58 | 59 | 74 | 75 | -------------------------------------------------------------------------------- /src/components/Calculator/CalculatorRenderBlock.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 23 | 24 | -------------------------------------------------------------------------------- /src/components/CloseButton.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | -------------------------------------------------------------------------------- /src/components/Cocktail/CocktailGridContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 27 | 28 | 44 | -------------------------------------------------------------------------------- /src/components/Cocktail/CocktailIngredientShare.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 53 | 54 | 79 | -------------------------------------------------------------------------------- /src/components/Cocktail/CocktailPrice.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/Cocktail/CocktailRating.vue: -------------------------------------------------------------------------------- 1 | 18 | 32 | 68 | -------------------------------------------------------------------------------- /src/components/Cocktail/CocktailThumb.vue: -------------------------------------------------------------------------------- 1 | 6 | 37 | 52 | -------------------------------------------------------------------------------- /src/components/Cocktail/GenerateImageDialog.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 95 | 96 | 106 | -------------------------------------------------------------------------------- /src/components/Cocktail/SimilarCocktails.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 54 | -------------------------------------------------------------------------------- /src/components/Collections/CollectionDetailsWidget.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/Collections/CollectionForm.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 86 | -------------------------------------------------------------------------------- /src/components/DateFormatter.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /src/components/Dialog/ConfirmDialog.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 67 | 68 | 95 | -------------------------------------------------------------------------------- /src/components/Dialog/SaltRimDialog.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 68 | -------------------------------------------------------------------------------- /src/components/Dialog/plugin.js: -------------------------------------------------------------------------------- 1 | import { dialogBus } from '@/composables/eventBus' 2 | 3 | export default { 4 | install: (app) => { 5 | app.config.globalProperties.$confirm = (message, dialogOptions) => { 6 | dialogBus.emit('requestConfirm', { body: message, ...dialogOptions }) 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/components/EmptyState.vue: -------------------------------------------------------------------------------- 1 | 13 | 43 | -------------------------------------------------------------------------------- /src/components/Feeds/FeedsIndex.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 48 | 49 | 62 | -------------------------------------------------------------------------------- /src/components/Feeds/FeedsRecipe.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 32 | 33 | -------------------------------------------------------------------------------- /src/components/GlassSelector.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 56 | 57 | -------------------------------------------------------------------------------- /src/components/Icons/IconBarShelf.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconCalculator.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconCheck.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconClose.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconCocktail.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconExternal.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconFavorite.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconJigger.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconMedal.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconMore.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconPublicLink.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconRecommender.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconShoppingCart.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconStar.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Icons/IconUserShelf.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/ImageEditor.vue: -------------------------------------------------------------------------------- 1 | 28 | 94 | 101 | -------------------------------------------------------------------------------- /src/components/ImageThumb.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 40 | 41 | 59 | -------------------------------------------------------------------------------- /src/components/Ingredient/Hierarchy/IngredientTreeNode.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | -------------------------------------------------------------------------------- /src/components/Ingredient/Hierarchy/IngredientTreeNodeItem.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 24 | -------------------------------------------------------------------------------- /src/components/Ingredient/IngredientGridContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 16 | 17 | 30 | -------------------------------------------------------------------------------- /src/components/Ingredient/IngredientHierarchy.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 63 | 64 | -------------------------------------------------------------------------------- /src/components/Ingredient/IngredientImage.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 35 | 80 | -------------------------------------------------------------------------------- /src/components/Ingredient/IngredientListContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 20 | -------------------------------------------------------------------------------- /src/components/Ingredient/IngredientSpotlight.vue: -------------------------------------------------------------------------------- 1 | 15 | 79 | 110 | -------------------------------------------------------------------------------- /src/components/Ingredient/RecommendedIngredients.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | -------------------------------------------------------------------------------- /src/components/Layout/SiteFooter.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 59 | 60 | 102 | -------------------------------------------------------------------------------- /src/components/Layout/SiteLogo.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 51 | -------------------------------------------------------------------------------- /src/components/ListItemContainer.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /src/components/MiniRating.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 17 | 18 | 34 | -------------------------------------------------------------------------------- /src/components/Note/NoteDetails.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 44 | 45 | 70 | -------------------------------------------------------------------------------- /src/components/Note/NoteDialog.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 63 | -------------------------------------------------------------------------------- /src/components/OverlayLoader.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 51 | -------------------------------------------------------------------------------- /src/components/PageHeader.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 69 | -------------------------------------------------------------------------------- /src/components/Print/PrintShoppingList.vue: -------------------------------------------------------------------------------- 1 | 17 | 49 | 50 | 98 | -------------------------------------------------------------------------------- /src/components/RatingActions.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 55 | 56 | 97 | -------------------------------------------------------------------------------- /src/components/SaltRimCheckbox.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | 32 | -------------------------------------------------------------------------------- /src/components/SaltRimDropdown.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 37 | -------------------------------------------------------------------------------- /src/components/SaltRimRadio.vue: -------------------------------------------------------------------------------- 1 | 13 | 46 | 111 | -------------------------------------------------------------------------------- /src/components/SaltRimSpinner.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 22 | 23 | 42 | -------------------------------------------------------------------------------- /src/components/Search/OffCanvas.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/Search/SearchPagination.vue: -------------------------------------------------------------------------------- 1 | 17 | 62 | -------------------------------------------------------------------------------- /src/components/Settings/CocktailMethodsForm.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 75 | -------------------------------------------------------------------------------- /src/components/Settings/PriceCategoryForm.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 87 | -------------------------------------------------------------------------------- /src/components/Settings/SettingsNavigation.vue: -------------------------------------------------------------------------------- 1 | 19 | 31 | 54 | -------------------------------------------------------------------------------- /src/components/Settings/TagForm.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 74 | -------------------------------------------------------------------------------- /src/components/Settings/UtensilForm.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 79 | -------------------------------------------------------------------------------- /src/components/SourcePresenter.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 25 | -------------------------------------------------------------------------------- /src/components/StatusCheck.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 29 | 30 | 43 | -------------------------------------------------------------------------------- /src/components/SubscriptionCheck.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 26 | -------------------------------------------------------------------------------- /src/components/ThemeToggle.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 52 | 53 | 79 | -------------------------------------------------------------------------------- /src/components/TimeStamps.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | 39 | -------------------------------------------------------------------------------- /src/components/ToggleIngredientBarShelf.vue: -------------------------------------------------------------------------------- 1 | 47 | 56 | -------------------------------------------------------------------------------- /src/components/ToggleIngredientShelf.vue: -------------------------------------------------------------------------------- 1 | 47 | 56 | -------------------------------------------------------------------------------- /src/components/ToggleIngredientShoppingCart.vue: -------------------------------------------------------------------------------- 1 | 50 | 59 | -------------------------------------------------------------------------------- /src/components/Units/UnitConverter.vue: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /src/components/Units/UnitPicker.vue: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /src/components/WakeLockToggle.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 50 | 51 | -------------------------------------------------------------------------------- /src/composables/__tests__/useHtmlDecode.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { useHtmlDecode } from '../useHtmlDecode'; 3 | 4 | describe('useHtmlDecode', () => { 5 | it('decodes a valid HTML-encoded string', () => { 6 | const input = '&'; 7 | const result = useHtmlDecode(input); 8 | expect(result).toBe('&'); 9 | }); 10 | 11 | it('returns an empty string when input is empty', () => { 12 | const input = ''; 13 | const result = useHtmlDecode(input); 14 | expect(result).toBe(''); 15 | }); 16 | 17 | it('returns the same string if there are no HTML entities', () => { 18 | const input = 'Hello, world!'; 19 | const result = useHtmlDecode(input); 20 | expect(result).toBe('Hello, world!'); 21 | }); 22 | 23 | it('decodes a string with multiple HTML entities', () => { 24 | const input = '<div>Hello & welcome!</div>'; 25 | const result = useHtmlDecode(input); 26 | expect(result).toBe('
Hello & welcome!
'); 27 | }); 28 | 29 | it('handles unsupported or invalid HTML entities gracefully', () => { 30 | const input = '&unknown;'; 31 | const result = useHtmlDecode(input); 32 | expect(result).toBe('&unknown;'); 33 | }); 34 | }); 35 | -------------------------------------------------------------------------------- /src/composables/__tests__/useRecommendedAmount.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, it, expect } from 'vitest'; 2 | import { useRecommendedAmounts } from './../useRecommendedAmounts'; 3 | 4 | describe('useRecommendedAmounts', () => { 5 | it('returns default amounts in ml when default unit is ml', () => { 6 | const { defaultAmounts } = useRecommendedAmounts('ml'); 7 | expect(defaultAmounts.value).toEqual(['7.5', '15', '22.5', '30', '37.5', '45', '52.5', '60']); 8 | }); 9 | 10 | it('converts default amounts to cl when default unit is cl', () => { 11 | const { defaultAmounts } = useRecommendedAmounts('cl'); 12 | expect(defaultAmounts.value).toEqual(['0.75', '1.5', '2.25', '3', '3.75', '4.5', '5.25', '6']); 13 | }); 14 | 15 | it('converts default amounts to oz as fractions when default unit is oz', () => { 16 | const { defaultAmounts } = useRecommendedAmounts('oz'); 17 | const expectedAmounts = ['1/4', '1/2', '3/4', '1', '1 1/4', '1 1/2', '1 3/4', '2']; 18 | expect(defaultAmounts.value).toEqual(expectedAmounts); 19 | }); 20 | 21 | it('handles an empty default unit gracefully', () => { 22 | const { defaultAmounts } = useRecommendedAmounts(''); 23 | expect(defaultAmounts.value).toEqual(['7.5', '15', '22.5', '30', '37.5', '45', '52.5', '60']); 24 | }); 25 | 26 | it('handles an unsupported default unit gracefully', () => { 27 | const { defaultAmounts } = useRecommendedAmounts('unsupported'); 28 | expect(defaultAmounts.value).toEqual(['7.5', '15', '22.5', '30', '37.5', '45', '52.5', '60']); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/composables/__tests__/useUnits.test.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from 'vitest' 2 | import { unitHandler } from './../useUnits' 3 | 4 | test('format numbers as fraction', () => { 5 | expect(unitHandler.asFraction(1.5)).toBe('1 1/2') 6 | expect(unitHandler.asFraction(1)).toBe('1') 7 | expect(unitHandler.asFraction(0.5)).toBe('1/2') 8 | }) 9 | 10 | test('format numbers as decimal', () => { 11 | expect(unitHandler.asDecimal('1 1/2')).toBe(1.5) 12 | expect(unitHandler.asDecimal('1')).toBe(1) 13 | expect(unitHandler.asDecimal('1/2')).toBe(0.5) 14 | }) 15 | 16 | test('ingredients are pretty printed', () => { 17 | expect(unitHandler.print({ amount: 30, units: 'ml' })).toBe('30 ml') 18 | expect(unitHandler.print({ amount: 30, amount_max: 60, units: 'ml' })).toBe('30-60 ml') 19 | expect(unitHandler.print({ amount: 30, units: 'ml' }, 'oz')).toBe('1 oz') 20 | expect(unitHandler.print({ amount: 1, amount_max: '1 1/2', units: 'oz' }, 'ml')).toBe('30-45 ml') 21 | expect(unitHandler.print({ amount: '1/8', units: 'teaspoon' })).toBe('0.125 teaspoon') 22 | expect(unitHandler.print({ amount: '500', units: 'unconvertable' }, 'cl')).toBe('500 unconvertable') 23 | expect(unitHandler.print({ amount: 60, units: 'ml' }, 'ml', 3)).toBe('180 ml') 24 | expect(unitHandler.print({ amount: null, units: 'ml' }, 'ml')).toBe('0 ml') 25 | expect(unitHandler.print({ amount: null, amount_max: null, units: 'ml' }, 'ml')).toBe('0 ml') 26 | expect(unitHandler.print({ amount: '0.125', units: 'oz' }, 'oz')).toBe('1/8 oz') 27 | }) 28 | 29 | test('numbers are fixed and truncated', () => { 30 | expect(unitHandler.toFixedWithTruncate(1.2333333333339, 2)).toBe(1.23) 31 | expect(unitHandler.toFixedWithTruncate(9.00, 3)).toBe(9) 32 | expect(unitHandler.toFixedWithTruncate(9, 3)).toBe(9) 33 | }) 34 | 35 | test('converts units', () => { 36 | expect(unitHandler.convertFromTo('oz', 1, 'ml')).toBe(30) 37 | expect(unitHandler.convertFromTo('oz', 1, 'cl')).toBe(3) 38 | expect(unitHandler.convertFromTo('ml', 30, 'oz')).toBe(1) 39 | expect(unitHandler.convertFromTo('ml', 30, 'cl')).toBe(3) 40 | expect(unitHandler.convertFromTo('cl', 3, 'oz')).toBe(1) 41 | expect(unitHandler.convertFromTo('cl', 3, 'ml')).toBe(30) 42 | }) 43 | 44 | test('formats prices', () => { 45 | expect(unitHandler.formatPrice(30, 'USD')).toBe('$30.00') 46 | expect(unitHandler.formatPrice(25.99, 'EUR')).toBe('€25.99') 47 | }) -------------------------------------------------------------------------------- /src/composables/confirm.ts: -------------------------------------------------------------------------------- 1 | import { dialogBus } from './eventBus' 2 | 3 | export function useConfirm() { 4 | return { 5 | show(message: string, dialogOptions: object): void { 6 | dialogBus.emit('requestConfirm', { body: message, ...dialogOptions }) 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /src/composables/eventBus.ts: -------------------------------------------------------------------------------- 1 | import { useEventBus } from '@vueuse/core' 2 | 3 | const dialogBus = useEventBus('dialogs') 4 | const barBus = useEventBus('bars') 5 | 6 | export { dialogBus, barBus } -------------------------------------------------------------------------------- /src/composables/ingredientBg.ts: -------------------------------------------------------------------------------- 1 | const useIngredientBg = (hex: string) => { 2 | let c: number | string[] | null = null; 3 | 4 | if (/^#([A-Fa-f0-9]{3}){1,2}$/.test(hex)) { 5 | c = hex.substring(1).split(''); 6 | 7 | if (c.length === 3) { 8 | c = [c[0], c[0], c[1], c[1], c[2], c[2]]; 9 | } 10 | 11 | const hexValue = Number('0x' + c.join('')); 12 | const red = (hexValue >> 16) & 255; 13 | const green = (hexValue >> 8) & 255; 14 | const blue = hexValue & 255; 15 | 16 | return `rgba(${red}, ${green}, ${blue}, 0.3)`; 17 | } 18 | 19 | return hex; 20 | } 21 | 22 | export { useIngredientBg } -------------------------------------------------------------------------------- /src/composables/title.ts: -------------------------------------------------------------------------------- 1 | import { useTitle as useVueTitle } from '@vueuse/core' 2 | import AppState from '@/AppState' 3 | 4 | const useTitle = (title: string) => { 5 | const appState = new AppState() 6 | 7 | useVueTitle(`${title} \u22C5 ${appState.bar.name ?? 'Bar Assistant'}`) 8 | } 9 | 10 | export { useTitle } -------------------------------------------------------------------------------- /src/composables/toast.ts: -------------------------------------------------------------------------------- 1 | import { useToast } from 'vue-toast-notification' 2 | 3 | export function useSaltRimToast() { 4 | return useToast({ 5 | position: 'top', 6 | type: 'default', 7 | duration: 1500, 8 | pauseOnHover: true, 9 | }) 10 | } -------------------------------------------------------------------------------- /src/composables/useAuth.ts: -------------------------------------------------------------------------------- 1 | import BarAssistantClient from '@/api/BarAssistantClient' 2 | import AppState from '@/AppState' 3 | 4 | const useAuth = async (token: string): Promise => { 5 | const appState = new AppState() 6 | appState.setToken(token) 7 | 8 | try { 9 | const profile = (await BarAssistantClient.getProfile())?.data 10 | if (!profile) { 11 | appState.forgetUser() 12 | return '/login' 13 | } 14 | 15 | appState.setUser(profile) 16 | 17 | if (profile.settings?.language) { 18 | appState.setLanguage(profile.settings.language) 19 | } 20 | 21 | if (profile.settings?.theme) { 22 | appState.setTheme(profile.settings.theme) 23 | } 24 | } catch (e: any) { 25 | appState.forgetUser() 26 | 27 | return '/login' 28 | } 29 | 30 | const bars = (await BarAssistantClient.getBars())?.data 31 | 32 | if (bars?.length == 1) { 33 | appState.setBar(bars[0]) 34 | 35 | return '/' 36 | } else { 37 | return '/bars' 38 | } 39 | } 40 | 41 | export { useAuth } -------------------------------------------------------------------------------- /src/composables/useGetRoleName.ts: -------------------------------------------------------------------------------- 1 | export function getRoleName(roleId: number): string|null { 2 | switch (roleId) { 3 | case 1: 4 | return 'admin' 5 | case 2: 6 | return 'moderator' 7 | case 3: 8 | return 'general' 9 | case 4: 10 | return 'guest' 11 | default: 12 | return null 13 | } 14 | } -------------------------------------------------------------------------------- /src/composables/useHtmlDecode.ts: -------------------------------------------------------------------------------- 1 | import { decode } from 'entities' 2 | 3 | export function useHtmlDecode(input: string): string { 4 | return decode(input) 5 | } 6 | -------------------------------------------------------------------------------- /src/composables/useRecommendedAmounts.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | import { unitHandler } from './useUnits'; 3 | 4 | export function useRecommendedAmounts(defaultUnit: string) { 5 | const defaultAmountsInMl = ['7.5', '15', '22.5', '30', '37.5', '45', '52.5', '60']; 6 | const defaultAmounts = ref(defaultAmountsInMl); 7 | 8 | if (defaultUnit === 'cl') { 9 | defaultAmounts.value = defaultAmountsInMl.map(amount => (parseFloat(amount) / 10).toString()); 10 | } else if (defaultUnit === 'oz') { 11 | defaultAmounts.value = defaultAmountsInMl.map(amount => unitHandler.asFraction(unitHandler.ml2oz(parseFloat(amount)))); 12 | } 13 | 14 | return { defaultAmounts }; 15 | } 16 | -------------------------------------------------------------------------------- /src/composables/useTheme.ts: -------------------------------------------------------------------------------- 1 | const metaThemeColor: Record = { 2 | 'dark': '#282238', 3 | 'light': '#584b75', 4 | } 5 | 6 | const useTheme = (theme: string): void => { 7 | const darkThemeCssClassName = `dark-theme` 8 | 9 | document.body.classList.remove(darkThemeCssClassName) 10 | if (theme === 'dark') { 11 | document.body.classList.add(darkThemeCssClassName) 12 | } else { 13 | document.body.classList.remove(darkThemeCssClassName) 14 | } 15 | 16 | document.querySelector('meta[name="theme-color"]')?.setAttribute('content', metaThemeColor[theme]) 17 | } 18 | 19 | export { useTheme } -------------------------------------------------------------------------------- /src/locales/de-DE.js: -------------------------------------------------------------------------------- 1 | import messages from './messages/de-DE.json' 2 | 3 | const datetime = { 4 | 'short': { 5 | 'year': 'numeric', 6 | 'month': 'short', 7 | 'day': 'numeric' 8 | }, 9 | 'long': { 10 | 'year': 'numeric', 11 | 'month': 'short', 12 | 'day': 'numeric', 13 | 'weekday': 'short', 14 | 'hour': 'numeric', 15 | 'minute': 'numeric' 16 | } 17 | } 18 | 19 | const numbers = { 20 | decimal: { 21 | style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 22 | } 23 | } 24 | 25 | export default { messages, datetime, numbers } 26 | -------------------------------------------------------------------------------- /src/locales/en-US.js: -------------------------------------------------------------------------------- 1 | import messages from './messages/en-US.json' 2 | 3 | const datetime = { 4 | 'short': { 5 | 'year': 'numeric', 6 | 'month': 'short', 7 | 'day': 'numeric' 8 | }, 9 | 'long': { 10 | 'year': 'numeric', 11 | 'month': 'short', 12 | 'day': 'numeric', 13 | 'weekday': 'short', 14 | 'hour': 'numeric', 15 | 'minute': 'numeric' 16 | } 17 | } 18 | 19 | const numbers = { 20 | decimal: { 21 | style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 22 | } 23 | } 24 | 25 | export default { messages, datetime, numbers } 26 | -------------------------------------------------------------------------------- /src/locales/fr-FR.js: -------------------------------------------------------------------------------- 1 | import messages from './messages/fr-FR.json' 2 | 3 | const datetime = { 4 | 'short': { 5 | 'year': 'numeric', 6 | 'month': 'short', 7 | 'day': 'numeric' 8 | }, 9 | 'long': { 10 | 'year': 'numeric', 11 | 'month': 'short', 12 | 'day': 'numeric', 13 | 'weekday': 'short', 14 | 'hour': 'numeric', 15 | 'minute': 'numeric' 16 | } 17 | } 18 | 19 | const numbers = { 20 | decimal: { 21 | style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 22 | } 23 | } 24 | 25 | export default { messages, datetime, numbers } 26 | -------------------------------------------------------------------------------- /src/locales/hr-HR.js: -------------------------------------------------------------------------------- 1 | import messages from './messages/hr-HR.json' 2 | 3 | const datetime = { 4 | 'short': { 5 | 'year': 'numeric', 6 | 'month': 'short', 7 | 'day': 'numeric' 8 | }, 9 | 'long': { 10 | 'year': 'numeric', 11 | 'month': 'short', 12 | 'day': 'numeric', 13 | 'hour': 'numeric', 14 | 'minute': 'numeric' 15 | } 16 | } 17 | 18 | const numbers = { 19 | decimal: { 20 | style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 21 | } 22 | } 23 | 24 | export default { messages, datetime, numbers } 25 | -------------------------------------------------------------------------------- /src/locales/it-IT.js: -------------------------------------------------------------------------------- 1 | import messages from './messages/it-IT.json' 2 | 3 | const datetime = { 4 | 'short': { 5 | 'year': 'numeric', 6 | 'month': 'short', 7 | 'day': 'numeric' 8 | }, 9 | 'long': { 10 | 'year': 'numeric', 11 | 'month': 'short', 12 | 'day': 'numeric', 13 | 'weekday': 'short', 14 | 'hour': 'numeric', 15 | 'minute': 'numeric' 16 | } 17 | } 18 | 19 | const numbers = { 20 | decimal: { 21 | style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 22 | } 23 | } 24 | 25 | export default { messages, datetime, numbers } 26 | -------------------------------------------------------------------------------- /src/locales/pl-PL.js: -------------------------------------------------------------------------------- 1 | import messages from './messages/pl-PL.json' 2 | 3 | const datetime = { 4 | 'short': { 5 | 'year': 'numeric', 6 | 'month': 'short', 7 | 'day': 'numeric' 8 | }, 9 | 'long': { 10 | 'year': 'numeric', 11 | 'month': 'short', 12 | 'day': 'numeric', 13 | 'weekday': 'short', 14 | 'hour': 'numeric', 15 | 'minute': 'numeric' 16 | } 17 | } 18 | 19 | const numbers = { 20 | decimal: { 21 | style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 22 | } 23 | } 24 | 25 | export default { messages, datetime, numbers } 26 | -------------------------------------------------------------------------------- /src/locales/sv-SE.js: -------------------------------------------------------------------------------- 1 | import messages from './messages/sv-SE.json' 2 | 3 | const datetime = { 4 | 'short': { 5 | 'year': 'numeric', 6 | 'month': 'short', 7 | 'day': 'numeric' 8 | }, 9 | 'long': { 10 | 'year': 'numeric', 11 | 'month': 'short', 12 | 'day': 'numeric', 13 | 'weekday': 'short', 14 | 'hour': 'numeric', 15 | 'minute': 'numeric' 16 | } 17 | } 18 | 19 | const numbers = { 20 | decimal: { 21 | style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 22 | } 23 | } 24 | 25 | export default { messages, datetime, numbers } 26 | -------------------------------------------------------------------------------- /src/locales/zh-CN.js: -------------------------------------------------------------------------------- 1 | import messages from './messages/zh-CN.json' 2 | 3 | const datetime = { 4 | 'short': { 5 | 'year': 'numeric', 6 | 'month': 'short', 7 | 'day': 'numeric' 8 | }, 9 | 'long': { 10 | 'year': 'numeric', 11 | 'month': 'short', 12 | 'day': 'numeric', 13 | 'weekday': 'short', 14 | 'hour': 'numeric', 15 | 'minute': 'numeric' 16 | } 17 | } 18 | 19 | const numbers = { 20 | decimal: { 21 | style: 'decimal', minimumFractionDigits: 2, maximumFractionDigits: 2 22 | } 23 | } 24 | 25 | export default { messages, datetime, numbers } 26 | -------------------------------------------------------------------------------- /src/views/BarFormView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/BarJoinView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/BarsView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/CalculatorFormView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/CalculatorsIndex.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/CocktailCollections.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/CocktailPrintView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/CocktailView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/CocktailsFormView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/CocktailsScrapeView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/CocktailsView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/ConfirmationView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/views/FeedsView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/ForgotPasswordView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/views/HomeView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/IngredientFormView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/IngredientImport.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/IngredientView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/IngredientsView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/LoginView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/views/MenuPublicView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/views/MenuView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/OauthCallbackView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/views/PageNotFound.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 54 | -------------------------------------------------------------------------------- /src/views/ProfileView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/PublicCocktailView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/views/QuantityCalcView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/RegisterView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/views/ResetPasswordView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | -------------------------------------------------------------------------------- /src/views/ServiceDownView.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /src/views/SettingsAPIView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/SettingsBillingView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/SettingsCocktailMethodsView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/SettingsExportsView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/SettingsGlassesView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/SettingsPriceCategoriesView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/SettingsProfileView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/SettingsTagsView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/SettingsUsersView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/SettingsUtensilsView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/ShelfShoppingListPrintView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/views/ShoppingListView.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.dom.json", 3 | "include": ["env.d.ts", "src/**/*", "src/**/*.vue"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | "composite": true, 7 | "allowJs": true, 8 | "checkJs": false, 9 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 10 | "paths": { 11 | "@/*": ["./src/*"] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { fileURLToPath, URL } from 'node:url' 2 | import { defineConfig } from 'vite' 3 | import vue from '@vitejs/plugin-vue' 4 | import { VitePWA } from 'vite-plugin-pwa' 5 | 6 | const manifest = { 7 | name: 'Bar Assistant', 8 | short_name: 'Bar Assistant', 9 | description: 'Bar assistant is a self hosted application for managing your home bar.', 10 | theme_color: '#584b75', 11 | orientation: "portrait", 12 | icons: [ 13 | { 14 | "src": 'icons/pwa256.png', 15 | "sizes": '256x256', 16 | "type": 'image/png', 17 | "purpose": "any", 18 | }, 19 | { 20 | "src": 'icons/pwa512.png', 21 | "sizes": '512x512', 22 | "type": 'image/png', 23 | "purpose": "any", 24 | }, 25 | { 26 | "purpose": "maskable", 27 | "sizes": "858x858", 28 | "src": "icons/maskable_icon.png", 29 | "type": "image/png" 30 | }, 31 | { 32 | "purpose": "maskable", 33 | "sizes": "48x48", 34 | "src": "icons/maskable_icon_x48.png", 35 | "type": "image/png" 36 | }, 37 | { 38 | "purpose": "maskable", 39 | "sizes": "128x128", 40 | "src": "icons/maskable_icon_x128.png", 41 | "type": "image/png" 42 | }, 43 | { 44 | "purpose": "maskable", 45 | "sizes": "512x512", 46 | "src": "icons/maskable_icon_x512.png", 47 | "type": "image/png" 48 | } 49 | ] 50 | }; 51 | 52 | // https://vitejs.dev/config/ 53 | export default defineConfig({ 54 | plugins: [vue({ 55 | template: { 56 | compilerOptions: { 57 | isCustomElement: (tag) => ['swiper-container', 'swiper-slide'].includes(tag), 58 | } 59 | } 60 | }), VitePWA({ 61 | workbox: { 62 | navigateFallbackDenylist: [/^\/bar/] 63 | }, 64 | manifest: manifest, 65 | registerType: 'autoUpdate', 66 | })], 67 | resolve: { 68 | alias: { 69 | '@': fileURLToPath(new URL('./src', import.meta.url)) 70 | } 71 | } 72 | }) 73 | --------------------------------------------------------------------------------