├── .dockerignore ├── .env.example ├── .eslintignore ├── .eslintrc.cjs ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── ci.yml │ └── codeql.yml ├── .gitignore ├── .npmrc ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── docker-compose.yml ├── faviconDescription.json ├── jsconfig.json ├── logo.svg ├── package-lock.json ├── package.json ├── playwright.config.js ├── src ├── app.d.ts ├── app.html ├── app.scss ├── css │ ├── dark-theme.scss │ ├── light-theme.scss │ └── styles.css ├── hooks.server.js ├── js │ ├── toast.js │ └── translations.js ├── lib │ ├── charts │ │ ├── PieChart.svelte │ │ └── StackedBarLineChart.svelte │ ├── forms │ │ ├── AlignSelect.svelte │ │ ├── Button.svelte │ │ ├── ColorInput.svelte │ │ ├── CoordinatesInput.svelte │ │ ├── DateInput.svelte │ │ ├── EmailInput.svelte │ │ ├── PasswordInput.svelte │ │ ├── PhoneInput.svelte │ │ ├── RangeInput.svelte │ │ ├── SearchInput.svelte │ │ ├── SocialIconLink.svelte │ │ ├── SocialIconTextInput.svelte │ │ ├── Switch.svelte │ │ ├── TextInput.svelte │ │ ├── TextareaInput.svelte │ │ ├── TimeZoneSelect.svelte │ │ └── UploadFile.svelte │ ├── layout │ │ ├── AdminMain.svelte │ │ ├── Divider.svelte │ │ ├── Heading.svelte │ │ ├── IconLink.svelte │ │ ├── NavbarTop.svelte │ │ ├── Preview.svelte │ │ └── menus │ │ │ ├── MenuDivider.svelte │ │ │ ├── desktop │ │ │ ├── DesktopMenu.svelte │ │ │ └── DesktopMenuItem.svelte │ │ │ └── mobile │ │ │ └── MobileMenu.svelte │ ├── logo │ │ └── Logo.svelte │ ├── maps │ │ └── Map.svelte │ ├── nfc │ │ └── Nfc.svelte │ ├── pagination │ │ └── Pagination.svelte │ ├── theme │ │ └── ThemeToggle.svelte │ └── vCard │ │ ├── BusinessCard.svelte │ │ ├── DisplayPreview.svelte │ │ ├── VCard.svelte │ │ └── views │ │ ├── MobileView.svelte │ │ └── ProductionView.svelte ├── routes │ ├── +error.svelte │ ├── +layout.svelte │ ├── +page.server.js │ ├── admin │ │ ├── +page.server.js │ │ ├── +page.svelte │ │ ├── themes │ │ │ ├── +page.server.js │ │ │ ├── +page.svelte │ │ │ └── [id] │ │ │ │ ├── +page.server.js │ │ │ │ └── +page.svelte │ │ ├── users │ │ │ ├── +page.server.js │ │ │ └── +page.svelte │ │ └── vcard │ │ │ ├── +page.server.js │ │ │ └── +page.svelte │ ├── login │ │ ├── +page.server.js │ │ └── +page.svelte │ ├── p │ │ └── [id] │ │ │ └── t │ │ │ └── [themeId] │ │ │ ├── +page.js │ │ │ └── +page.svelte │ ├── qr │ │ └── [id] │ │ │ └── t │ │ │ └── [themeId] │ │ │ ├── +page.js │ │ │ └── +page.svelte │ └── reset │ │ ├── +page.server.js │ │ └── +page.svelte └── variables.scss ├── static ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-194x194.png ├── favicon-32x32.png ├── favicon.ico ├── js │ └── bootstrap.bundle.min.js ├── mstile-150x150.png ├── robots.txt ├── safari-pinned-tab.svg └── site.webmanifest ├── svelte.config.js ├── tests └── test.js └── vite.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | fly.toml 2 | Dockerfile 3 | .dockerignore 4 | node_modules 5 | .git 6 | .idea 7 | .env 8 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PUBLIC_REST_API_URL= 2 | PUBLIC_BASE_URL= 3 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | /static 10 | /test-results 11 | 12 | # Ignore files for PNPM, NPM and YARN 13 | pnpm-lock.yaml 14 | package-lock.json 15 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2022: true 5 | }, 6 | extends: 'airbnb-base', 7 | overrides: [], 8 | parserOptions: { 9 | ecmaVersion: 'latest', 10 | sourceType: 'module' 11 | }, 12 | rules: { 13 | 'import/no-unresolved': 'off', 14 | 'import/extensions': 'off', 15 | 'import/prefer-default-export': 'off', 16 | 'import/no-extraneous-dependencies': 'off', 17 | }, 18 | }; 19 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - MathiasReker 3 | 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: MathiasReker 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 16 | 1. Go to '...' 17 | 2. Click on '....' 18 | 3. Scroll down to '....' 19 | 4. See error 20 | 21 | **Expected behavior** 22 | A clear and concise description of what you expected to happen. 23 | 24 | **Screenshots** 25 | If applicable, add screenshots to help explain your problem. 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: MathiasReker 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'npm' 4 | directory: '/' 5 | schedule: 6 | interval: 'daily' 7 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: 18 | - '18.x' 19 | 20 | steps: 21 | - uses: actions/checkout@v3 22 | 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | cache: 'npm' 28 | 29 | - name: Set up environment variables 30 | run: | 31 | echo PUBLIC_REST_API_URL="http://localhost:8080" >> $GITHUB_ENV 32 | echo PUBLIC_BASE_URL="http://localhost:5173" >> $GITHUB_ENV 33 | 34 | - run: npm ci 35 | 36 | - run: npx playwright install 37 | 38 | # - run: npm test 39 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: CodeQL 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | schedule: 11 | - cron: '22 4 * * 3' 12 | 13 | jobs: 14 | analyze: 15 | name: Analyze 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 360 18 | permissions: 19 | actions: read 20 | contents: read 21 | security-events: write 22 | 23 | strategy: 24 | fail-fast: false 25 | matrix: 26 | language: 27 | - 'javascript' 28 | 29 | steps: 30 | - name: Checkout repository 31 | uses: actions/checkout@v3 32 | 33 | - name: Initialize CodeQL 34 | uses: github/codeql-action/init@v2 35 | with: 36 | languages: ${{ matrix.language }} 37 | 38 | - name: Autobuild 39 | uses: github/codeql-action/autobuild@v2 40 | 41 | - name: Perform CodeQL Analysis 42 | uses: github/codeql-action/analyze@v2 43 | with: 44 | category: "/language:${{ matrix.language }}" 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env 7 | .env.* 8 | !.env.example 9 | .vercel 10 | .output 11 | vite.config.js.timestamp-* 12 | vite.config.ts.timestamp-* 13 | /.idea 14 | /test-results 15 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | resolution-mode=highest 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make 6 | participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, 7 | disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, 8 | socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 9 | 10 | ## Our Standards 11 | 12 | Examples of behavior that contributes to creating a positive environment include: 13 | 14 | - Using welcoming and inclusive language 15 | - Being respectful of differing viewpoints and experiences 16 | - Gracefully accepting constructive criticism 17 | - Focusing on what is best for the community 18 | - Showing empathy towards other community members 19 | 20 | Examples of unacceptable behavior by participants include: 21 | 22 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 23 | - Trolling, insulting/derogatory comments, and personal or political attacks 24 | - Public or private harassment 25 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 26 | - Other conduct which could reasonably be considered inappropriate in a professional setting 27 | 28 | ## Our Responsibilities 29 | 30 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take 31 | appropriate and fair corrective action in response to any instances of unacceptable behavior. 32 | 33 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, 34 | issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any 35 | contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 36 | 37 | ## Scope 38 | 39 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the 40 | project or its community. Examples of representing a project or community include using an official project e-mail 41 | address, posting via an official social media account, or acting as an appointed representative at an online or offline 42 | event. Representation of a project may be further defined and clarified by project maintainers. 43 | 44 | ## Enforcement 45 | 46 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team (add 47 | contact method). All complaints will be reviewed and investigated and will result in a response that is deemed necessary 48 | and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the 49 | reporter of an incident. Further details of specific enforcement policies may be posted separately. 50 | 51 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent 52 | repercussions as determined by other members of the project's leadership. 53 | 54 | ## Attribution 55 | 56 | This Code of Conduct is adapted from the [Contributor Covenant](https://contributor-covenant.org), version 2.0, 57 | available at https://contributor-covenant.org/version/2/0/code_of_conduct.html. -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to CardMesh 2 | 3 | First off, thanks for taking the time to contribute! 🎉👍 4 | 5 | Contributions, bug reports, and issues are very welcome. Open source is about collaboration, and we are always looking 6 | for more collaborators! 7 | 8 | ## Code of Conduct 9 | 10 | By participating in this project, you are expected to uphold 11 | our [Code of Conduct](https://github.com/CardMesh/web-app/blob/main/CODE_OF_CONDUCT.md). 12 | 13 | ## Reporting Bugs 14 | 15 | If you find a bug, please feel free to [open an issue](https://github.com/CardMesh/web-app/issues/new/choose). A good 16 | bug report has: 17 | 18 | ## Suggesting Enhancements 19 | 20 | We love to hear about new ideas! If you have a suggestion for improving the app, 21 | please [open an issue](https://github.com/CardMesh/web-app/issues/new/choose). 22 | 23 | ## Your First Code Contribution 24 | 25 | If you're not sure where to start, look for issues tagged with 'good first issue'. These are usually small bugs or 26 | enhancements that have been specifically marked as friendly to people who are new to the codebase. 27 | 28 | ## Pull Requests 29 | 30 | 1. Fork the project, clone your fork, and configure the remotes 31 | 2. Create a new topic branch (from the main branch) to contain your feature, change, or fix. 32 | 3. Commit your changes in logical chunks. Make sure your commit messages are in 33 | the [proper format](https://chris.beams.io/posts/git-commit/). 34 | 4. Push your topic branch up to your fork. 35 | 5. [Open a Pull Request](https://github.com/CardMesh/web-app/compare) with a clear title and description. 36 | 37 | ## Setting Up Your Environment 38 | 39 | 1. Clone the application 40 | 41 | git clone git@github.com:CardMesh/web-app.git && cd web-app 42 | 43 | 2. Configure your private `.env` file, following the `.env.example` sample 44 | 45 | 3. Run the app 46 | 47 | npm run dev # dev 48 | 49 | npm run build && npm run preview # dev 50 | 51 | npm run build && npm run start # prod 52 | 53 | ## Running Tests 54 | 55 | Run the tests with the following command: 56 | 57 | npm run test 58 | 59 | Please ensure that the tests all pass before you submit a Pull Request. 60 | 61 | Thank you for contributing! 🎉 62 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1 2 | 3 | # Adjust NODE_VERSION as desired 4 | ARG NODE_VERSION=20.3.0 5 | FROM node:${NODE_VERSION}-slim AS base 6 | 7 | LABEL fly_launch_runtime="NodeJS" 8 | 9 | # NodeJS app lives here 10 | WORKDIR /app 11 | 12 | # Set production environment 13 | ENV NODE_ENV=production 14 | 15 | # Throw-away build stage to reduce size of final image 16 | FROM base AS build 17 | 18 | # Install node modules 19 | COPY --link package.json package-lock.json ./ 20 | RUN npm install --production=false 21 | 22 | # Copy application code 23 | COPY --link . . 24 | 25 | # Build application 26 | RUN npm run build 27 | 28 | # Remove development dependencies 29 | RUN npm prune --production 30 | 31 | # Final stage for app image 32 | FROM base 33 | 34 | # Copy built application 35 | COPY --from=build /app /app 36 | 37 | # Rename .env.production to .env 38 | RUN mv .env.production .env 39 | 40 | # Start the server by default, this can be overwritten at runtime 41 | CMD [ "npm", "run", "start" ] 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 CardMesh 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Web app for CardMesh

2 | 3 | [![CI status](https://github.com/CardMesh/web-app/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/CardMesh/web-app/actions/workflows/ci.yml) 4 | [![Contributors](https://img.shields.io/github/contributors/CardMesh/web-app.svg)](https://github.com/CardMesh/web-app/graphs/contributors) 5 | [![Forks](https://img.shields.io/github/forks/CardMesh/web-app.svg)](https://github.com/CardMesh/web-app/network/members) 6 | [![Stargazers](https://img.shields.io/github/stars/CardMesh/web-app.svg)](https://github.com/CardMesh/web-app/stargazers) 7 | [![Issues](https://img.shields.io/github/issues/CardMesh/web-app.svg)](https://github.com/CardMesh/web-app/issues) 8 | [![MIT License](https://img.shields.io/github/license/CardMesh/web-app.svg)](https://github.com/CardMesh/web-app/blob/main/LICENSE) 9 | 10 | CardMesh is an app aimed at modernizing the sharing of business cards within a company. It displays digital business 11 | cards in a web browser, accessible via NFC tags, QR codes, or direct URLs. 12 | 13 | ### Tech Stack 14 | 15 | [![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=flat&logo=html5&logoColor=white)](#) 16 | [![CSS3](https://img.shields.io/badge/CSS3-1572B6?logo=css3&style=flat&logoColor=fff)](#) 17 | [![Sass](https://img.shields.io/badge/Sass-C69?logo=sass&logoColor=fff&style=flat)](#) 18 | [![Javascript](https://img.shields.io/badge/javascript-%23323330.svg?style=flat&logo=javascript&logoColor=%23F7DF1E)](#) 19 | [![Node.Js](https://img.shields.io/badge/Node.js-339933.svg?style=flat&logo=nodedotjs&logoColor=white)](#) 20 | [![Svelte](https://img.shields.io/badge/Svelte-FF3E00?logo=svelte&style=flat&logoColor=fff)](#) 21 | [![Vite](https://img.shields.io/badge/Vite-646CFF?logo=vite&logoColor=fff&style=flat)](#) 22 | [![Bootstrap](https://img.shields.io/badge/bootstrap-%23563D7C.svg?style=flat&logo=bootstrap&logoColor=white)](#) 23 | [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=fff&style=flat)](#) 24 | [![Docker](https://img.shields.io/badge/docker-%230db7ed.svg?style=flat&logo=docker&logoColor=white)](#) 25 | 26 | ### Versions & Dependencies 27 | 28 | | Version | Documentation | 29 | |---------|---------------| 30 | | 1.0.4 | current | 31 | 32 | ### Requirements 33 | 34 | - `Node.js` >= 18.x 35 | 36 | ## Documentation 37 | 38 | ## Steps to setup 39 | 40 | **1. Clone the application** 41 | 42 | ```bash 43 | git clone git@github.com:CardMesh/web-app.git && cd web-app 44 | ``` 45 | 46 | **2. Configure your private `.env` file, following the `.env.example` sample** 47 | 48 | **3. In `svelte.config.js` you can define the adapter. 49 | If you want to run this on a node server, use:** 50 | 51 | ```javascript 52 | import adapter from '@sveltejs/adapter-node'; 53 | ``` 54 | 55 | **4. Run the app** 56 | 57 | ```bash 58 | npm run dev # dev 59 | ``` 60 | 61 | ```bash 62 | npm run build && npm run preview # dev 63 | ``` 64 | 65 | ```bash 66 | npm run build && npm run start # prod 67 | ``` 68 | 69 | #### Docker 70 | 71 | If you're considering deploying using docker-compose, here's a simple example. Please see `./docker-compose.yml`. 72 | 73 | ```bash 74 | docker-compose up -d 75 | ``` 76 | 77 | ### Roadmap 78 | 79 | See the [open issues](https://github.com/CardMesh/web-app/issues) for a complete list of proposed 80 | features (and known issues). 81 | 82 | ### Contributing 83 | 84 | If you have a suggestion to enhance this project, kindly fork the repository and create a pull request. Alternatively, 85 | you may open an issue and tag it as "enhancement". Lastly, do not hesitate to give the project a star ⭐. Thank you for 86 | your support. 87 | 88 | #### Tools 89 | 90 | Coding standards checker: 91 | 92 | ```bash 93 | npm run lint 94 | ``` 95 | 96 | Coding standards fixer: 97 | 98 | ```bash 99 | npm run format 100 | ``` 101 | 102 | Unit tests: 103 | 104 | ```bash 105 | npm run test 106 | ``` 107 | 108 | #### Build tools 109 | 110 | Move bootstrap to `./static`. Used when bootstrap is updated: 111 | 112 | ```bash 113 | npm run build:bootstrap 114 | ``` 115 | 116 | Build all favicons: 117 | 118 | ```bash 119 | npm run build:favicons 120 | ``` 121 | 122 | ### License 123 | 124 | The distribution of the package operates under the `MIT License`. Further information can be found in the LICENSE file. 125 | 126 | ### DISCLAIMER: USE OF THIS GITHUB REPOSITORY 127 | 128 | By accessing and using this GitHub repository ("Repository"), you agree to the following terms and conditions. If you do 129 | not agree with any of these terms, please refrain from using the Repository. 130 | 131 | 1) No Warranty or Liability: 132 | The Repository is provided on an "as is" basis, without any warranties or representations of any kind, either 133 | expressed or implied. The owner(s) of the Repository ("Owner") hereby disclaim(s) any and all liability for any 134 | damages, losses, or injuries arising out of or in connection with the use, inability to use, or reliance on the 135 | Repository. 136 | 137 | 2) No Legal Advice: 138 | The content and information provided in the Repository are for informational purposes only and do not constitute 139 | legal advice. The Owner does not assume any responsibility for any actions taken or not taken based on the 140 | information provided in the Repository. For legal advice or specific inquiries, consult a qualified legal 141 | professional. 142 | 143 | 3) Intellectual Property: 144 | The Repository may contain copyrighted materials, including but not limited to code, documentation, images, and other 145 | intellectual property owned by the Owner or third parties. You may not use, copy, distribute, or modify any such 146 | materials without obtaining prior written permission from the respective copyright holder(s). 147 | 148 | 4) External Links: 149 | The Repository may include links to third-party websites or resources. The Owner does not endorse, control, or assume 150 | any responsibility for the content or practices of these third-party websites or resources. Accessing and using such 151 | links are solely at your own risk. 152 | 153 | 5) Modification of Repository: 154 | The Owner reserves the right to modify, update, or remove any content or functionality of the Repository at any time 155 | without prior notice. The Owner shall not be liable for any consequences arising from such modifications. 156 | 157 | 6) Indemnification: 158 | You agree to indemnify and hold the Owner harmless from and against any claims, damages, liabilities, costs, and 159 | expenses arising out of or in connection with your use of the Repository, including but not limited to any violation 160 | of these terms. 161 | 162 | 7) Governing Law: 163 | These terms shall be governed by and construed in accordance with the laws of the jurisdiction where the Owner is 164 | located, without regard to its conflict of law principles. 165 | 166 | By using the Repository, you acknowledge that you have read, understood, and agreed to these terms and conditions. If 167 | you do not agree with any of these terms, your sole remedy is to discontinue using the Repository. 168 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | We appreciate your dedication to the safety of CardMesh. The security of our extension is our paramount concern, and 4 | community assistance is invaluable. 5 | 6 | # Vulnerability Reporting 7 | 8 | If you suspect a vulnerability, we encourage you to share it with us by sending an email 9 | to [@MathiasReker](https://github.com/MathiasReker) at 10 | github@reker.dk. Please refrain from publicizing the issue before we have thoroughly investigated and resolved it. Our 11 | commitment is to acknowledge your report promptly and maintain clear communication throughout the process. 12 | 13 | Thank you for your contribution in enhancing CardMesh's security! 14 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | env_file: 8 | - .env.production 9 | ports: 10 | - "5173:5173" 11 | environment: 12 | - NODE_ENV=production 13 | - PORT=5173 14 | - ORIGIN=http://localhost:5173 15 | -------------------------------------------------------------------------------- /faviconDescription.json: -------------------------------------------------------------------------------- 1 | { 2 | "masterPicture": "./logo.svg", 3 | "iconsPath": "/", 4 | "design": { 5 | "ios": { 6 | "pictureAspect": "backgroundAndMargin", 7 | "backgroundColor": "#74aa9c", 8 | "margin": "28%", 9 | "assets": { 10 | "ios6AndPriorIcons": false, 11 | "ios7AndLaterIcons": false, 12 | "precomposedIcons": false, 13 | "declareOnlyDefaultIcon": true 14 | }, 15 | "appName": "CardMesh" 16 | }, 17 | "desktopBrowser": { 18 | "design": "background", 19 | "backgroundColor": "#74aa9c", 20 | "backgroundRadius": 0.45, 21 | "imageScale": 0.8 22 | }, 23 | "windows": { 24 | "pictureAspect": "whiteSilhouette", 25 | "backgroundColor": "#74aa9c", 26 | "onConflict": "override", 27 | "assets": { 28 | "windows80Ie10Tile": false, 29 | "windows10Ie11EdgeTiles": { 30 | "small": false, 31 | "medium": true, 32 | "big": false, 33 | "rectangle": false 34 | } 35 | }, 36 | "appName": "CardMesh" 37 | }, 38 | "androidChrome": { 39 | "pictureAspect": "shadow", 40 | "themeColor": "#74aa9c", 41 | "manifest": { 42 | "name": "CardMesh", 43 | "display": "standalone", 44 | "orientation": "notSet", 45 | "onConflict": "override", 46 | "declared": true 47 | }, 48 | "assets": { 49 | "legacyIcon": true, 50 | "lowResolutionIcons": false 51 | } 52 | }, 53 | "safariPinnedTab": { 54 | "pictureAspect": "silhouette", 55 | "themeColor": "#74aa9c" 56 | } 57 | }, 58 | "settings": { 59 | "compression": 5, 60 | "scalingAlgorithm": "Mitchell", 61 | "errorOnImageTooSmall": false, 62 | "readmeFile": false, 63 | "htmlCodeFile": false, 64 | "usePathAsIs": false 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "esModuleInterop": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "resolveJsonModule": true, 9 | "skipLibCheck": true, 10 | "sourceMap": true, 11 | "strict": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cardmesh-web-app", 3 | "version": "1.0.4", 4 | "description": "CardMesh is an app aimed at modernizing the sharing of business cards within a company. It displays digital business cards in a web browser, accessible via NFC tags, QR codes, or direct URLs.", 5 | "author": "Mathias Reker", 6 | "license": "MIT", 7 | "type": "module", 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:CardMesh/web-app.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/CardMesh/web-app/issues" 14 | }, 15 | "homepage": "https://github.com/CardMesh/web-app", 16 | "scripts": { 17 | "start": "node build", 18 | "dev": "vite dev", 19 | "build": "vite build", 20 | "preview": "vite preview", 21 | "build:bootstrap": "cross-env mkdir -p ./static/js/ && cp ./node_modules/bootstrap/dist/js/bootstrap.bundle.min.js ./static/js/", 22 | "build:favicons": "real-favicon generate faviconDescription.json faviconData.json static/ && rm faviconData.json", 23 | "test": "playwright test", 24 | "test:unit": "vitest", 25 | "check": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json", 26 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./jsconfig.json --watch", 27 | "format": "eslint . --fix", 28 | "lint": "eslint ." 29 | }, 30 | "devDependencies": { 31 | "@fontsource/fira-mono": "^5.2.6", 32 | "@neoconfetti/svelte": "^2.2.1", 33 | "@playwright/test": "^1.50.0", 34 | "@popperjs/core": "^2.11.8", 35 | "@rodneylab/svelte-social-icons": "^0.0.31", 36 | "@sveltejs/adapter-vercel": "^5.7.1", 37 | "@sveltejs/kit": "^2.20.8", 38 | "@types/cookie": "^1.0.0", 39 | "@zerodevx/svelte-toast": "^0.9.6", 40 | "bootstrap": "^5.3.3", 41 | "chart.js": "^4.4.8", 42 | "country-telephone-data": "^0.6.3", 43 | "cross-env": "^7.0.3", 44 | "eslint": "^9.28.0", 45 | "eslint-config-airbnb-base": "^15.0.0", 46 | "eslint-plugin-import": "^2.31.0", 47 | "eslint-plugin-svelte": "^3.3.3", 48 | "js-cookie": "^3.0.5", 49 | "leaflet": "^1.9.4", 50 | "moment-timezone": "^0.5.47", 51 | "qrcode-svg": "^1.1.0", 52 | "sass": "^1.86.0", 53 | "semver": "^7.7.1", 54 | "svelte": "^5.19.4", 55 | "svelte-check": "^4.1.4", 56 | "svelte-feather-icons": "^4.2.0", 57 | "svelte-icons-pack": "^3.1.3", 58 | "svelte-preprocess": "^6.0.3", 59 | "svelte-seo": "^1.6.1", 60 | "typescript": "^5.7.2", 61 | "vite": "^6.3.4", 62 | "vitest": "^3.1.3" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /playwright.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@playwright/test').PlaywrightTestConfig} */ 2 | const config = { 3 | webServer: { 4 | command: 'npm run build && npm run preview', 5 | port: 4173, 6 | }, 7 | testDir: 'tests', 8 | testMatch: /(.+\.)?(test|spec)\.[jt]s/, 9 | }; 10 | 11 | export default config; 12 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | } 6 | } 7 | 8 | export {}; 9 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | %sveltekit.head% 20 | 24 | 25 | 26 |
%sveltekit.body%
27 | 28 | 29 | -------------------------------------------------------------------------------- /src/app.scss: -------------------------------------------------------------------------------- 1 | // Defaults 2 | $primary: #1c4e80; 3 | $secondary: #6e7781; 4 | $success: #6bb187; 5 | $info: #0091d5; 6 | $warning: #dbae59; 7 | $danger: #ea6947; 8 | $light: #d2d2d2; 9 | $dark: #212529; 10 | $border-radius: 0.2rem; 11 | $border-radius-lg: 0.2rem; 12 | $color-mode-type: data; 13 | $card-border-radius: 10px; 14 | 15 | // Loads variables 16 | @import 'bootstrap/scss/functions'; 17 | @import 'bootstrap/scss/variables'; 18 | @import 'bootstrap/scss/variables-dark'; 19 | 20 | // Style alerts 21 | .alert { 22 | box-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); 23 | border: none !important; 24 | } 25 | 26 | // Load overrides 27 | @import './css/dark-theme'; 28 | @import './css/light-theme'; 29 | 30 | // Bootstrap 31 | @import 'bootstrap/scss/bootstrap'; 32 | 33 | // Style toasts 34 | :root { 35 | --toastBoxShadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1); 36 | } 37 | -------------------------------------------------------------------------------- /src/css/dark-theme.scss: -------------------------------------------------------------------------------- 1 | // scss-docs-start theme-text-dark-variables 2 | $primary-text-emphasis-dark: #b8dcff; 3 | $secondary-text-emphasis-dark: #c8d3e0; 4 | $success-text-emphasis-dark: #003917; 5 | $info-text-emphasis-dark: #c4ecff; 6 | $warning-text-emphasis-dark: #3e2800; 7 | $danger-text-emphasis-dark: #5b1103; 8 | $light-text-emphasis-dark: $gray-100; 9 | $dark-text-emphasis-dark: $gray-300; 10 | // scss-docs-end theme-text-dark-variables 11 | 12 | // scss-docs-start theme-bg-subtle-dark-variables 13 | $primary-bg-subtle-dark: $primary; 14 | $secondary-bg-subtle-dark: $secondary; 15 | $success-bg-subtle-dark: $success; 16 | $info-bg-subtle-dark: $info; 17 | $warning-bg-subtle-dark: $warning; 18 | $danger-bg-subtle-dark: $danger; 19 | $light-bg-subtle-dark: $gray-800; 20 | $dark-bg-subtle-dark: mix($gray-800, $black); 21 | // scss-docs-end theme-bg-subtle-dark-variables 22 | 23 | // scss-docs-start theme-border-subtle-dark-variables 24 | $primary-border-subtle-dark: shade-color($primary, 40%); 25 | $secondary-border-subtle-dark: shade-color($secondary, 40%); 26 | $success-border-subtle-dark: shade-color($success, 40%); 27 | $info-border-subtle-dark: shade-color($info, 40%); 28 | $warning-border-subtle-dark: shade-color($warning, 40%); 29 | $danger-border-subtle-dark: shade-color($danger, 40%); 30 | $light-border-subtle-dark: $gray-700; 31 | $dark-border-subtle-dark: $gray-800; 32 | 33 | // scss-docs-end theme-border-subtle-dark-variables 34 | $body-color-dark: $gray-500; 35 | $body-bg-dark: $gray-900; 36 | $body-secondary-color-dark: rgba($body-color-dark, 0.75); 37 | $body-secondary-bg-dark: $gray-800; 38 | $body-tertiary-color-dark: rgba($body-color-dark, 0.5); 39 | $body-tertiary-bg-dark: mix($gray-800, $gray-900, 50%); 40 | $body-emphasis-color-dark: $white; 41 | $border-color-dark: $gray-700; 42 | $border-color-translucent-dark: rgba($white, 0.15); 43 | $headings-color-dark: null; 44 | $link-color-dark: tint-color($primary, 40%); 45 | $link-hover-color-dark: shift-color($link-color-dark, -$link-shade-percentage); 46 | $code-color-dark: tint-color($code-color, 40%); 47 | 48 | // 49 | // Forms 50 | // 51 | $form-select-indicator-color-dark: $body-color-dark; 52 | 53 | $form-switch-color-dark: rgba($white, 0.25); 54 | // scss-docs-start form-validation-colors-dark 55 | $form-valid-color-dark: $success; 56 | $form-valid-border-color-dark: $green-300; 57 | $form-invalid-color-dark: $red-300; 58 | $form-invalid-border-color-dark: $red-300; 59 | // scss-docs-end form-validation-colors-dark 60 | 61 | // 62 | // Accordion 63 | // 64 | $accordion-icon-color-dark: $primary-text-emphasis-dark; 65 | $accordion-icon-active-color-dark: $primary-text-emphasis-dark; 66 | -------------------------------------------------------------------------------- /src/css/light-theme.scss: -------------------------------------------------------------------------------- 1 | // scss-docs-start theme-text-variables 2 | $primary-text-emphasis: #b8dcff; 3 | $secondary-text-emphasis: #c8d3e0; 4 | $success-text-emphasis: #003917; 5 | $info-text-emphasis: #c4ecff; 6 | $warning-text-emphasis: #3e2800; 7 | $danger-text-emphasis: #5b1103; 8 | $light-text-emphasis: $gray-100; 9 | $dark-text-emphasis: $gray-300; 10 | // scss-docs-end theme-text-variables 11 | 12 | // scss-docs-start theme-bg-subtle-variables 13 | $primary-bg-subtle: $primary; 14 | $secondary-bg-subtle: $secondary; 15 | $success-bg-subtle: $success; 16 | $info-bg-subtle: $info; 17 | $warning-bg-subtle: $warning; 18 | $danger-bg-subtle: $danger; 19 | $light-bg-subtle: $gray-800; 20 | $dark-bg-subtle: mix($gray-800, $black); 21 | $body-bg: $gray-400; 22 | $body-tertiary-bg: $gray-300; 23 | $border-color: $gray-600; 24 | // scss-docs-end theme-bg-subtle-variables 25 | -------------------------------------------------------------------------------- /src/css/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | min-height: 100vh; 3 | min-height: -webkit-fill-available; 4 | } 5 | 6 | html { 7 | height: -webkit-fill-available; 8 | } 9 | 10 | .dropdown-toggle { 11 | outline: 0; 12 | } 13 | 14 | .btn-toggle { 15 | padding: 0.25rem 0.5rem; 16 | font-weight: 600; 17 | color: var(--bs-emphasis-color); 18 | background-color: transparent; 19 | } 20 | 21 | .btn-toggle:hover, 22 | .btn-toggle:focus { 23 | color: rgba(var(--bs-emphasis-color-rgb), 0.85); 24 | background-color: var(--bs-tertiary-bg); 25 | } 26 | 27 | .btn-toggle::before { 28 | width: 1.25em; 29 | line-height: 0; 30 | content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%280,0,0,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); 31 | transition: transform 0.35s ease; 32 | transform-origin: 0.5em 50%; 33 | } 34 | 35 | [data-bs-theme='dark'] .btn-toggle::before { 36 | content: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='rgba%28255,255,255,.5%29' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e"); 37 | } 38 | 39 | .btn-toggle[aria-expanded='true'] { 40 | color: rgba(var(--bs-emphasis-color-rgb), 0.85); 41 | } 42 | 43 | .btn-toggle[aria-expanded='true']::before { 44 | transform: rotate(90deg); 45 | } 46 | 47 | .btn-toggle-nav a { 48 | padding: 0.1875rem 0.5rem; 49 | margin-top: 0.125rem; 50 | margin-left: 1.25rem; 51 | } 52 | 53 | .btn-toggle-nav a:hover, 54 | .btn-toggle-nav a:focus { 55 | background-color: var(--bs-tertiary-bg); 56 | } 57 | 58 | .scrollarea { 59 | overflow-y: auto; 60 | } 61 | -------------------------------------------------------------------------------- /src/hooks.server.js: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | 3 | export async function handle({ 4 | event, 5 | resolve, 6 | }) { 7 | const user = event.cookies.get('user') ?? '{}'; 8 | if (user === '{}') { 9 | event.cookies.delete('access'); 10 | } 11 | 12 | const access = event.cookies.get('access') ?? '{}'; 13 | if (event.url.pathname.startsWith('/admin') && (!JSON.parse(access)?.data?.token || !JSON.parse(user)?.data)) { 14 | throw redirect(302, '/login'); 15 | } 16 | 17 | const theme = JSON.parse(user)?.data?.theme || 'dark'; 18 | 19 | return resolve(event, { 20 | transformPageChunk: ({ html }) => html.replace('data-bs-theme="auto"', `data-bs-theme="${theme}"`), 21 | }); 22 | } 23 | -------------------------------------------------------------------------------- /src/js/toast.js: -------------------------------------------------------------------------------- 1 | import { toast } from '@zerodevx/svelte-toast'; 2 | 3 | export const displaySuccess = (message) => toast.push(message, { 4 | theme: { 5 | '--toastColor': '#003917', 6 | '--toastBackground': '#6bb187', 7 | '--toastBarBackground': '#003917', 8 | }, 9 | }); 10 | 11 | export const displayError = (message) => toast.push(message, { 12 | theme: { 13 | '--toastColor': '#5b1103', 14 | '--toastBackground': '#ea6947', 15 | '--toastBarBackground': '#5b1103', 16 | }, 17 | }); 18 | 19 | export const displayInfo = (message) => toast.push(message, { 20 | theme: { 21 | '--toastColor': '#b8dcff', 22 | '--toastBackground': '#0091d5', 23 | '--toastBarBackground': '#b8dcff', 24 | }, 25 | }); 26 | 27 | export const displayWarning = (message) => toast.push(message, { 28 | theme: { 29 | '--toastColor': '#3e2800', 30 | '--toastBackground': '#dbae59', 31 | '--toastBarBackground': '#3e2800', 32 | }, 33 | }); 34 | -------------------------------------------------------------------------------- /src/js/translations.js: -------------------------------------------------------------------------------- 1 | export const browserLanguage = navigator.language.substring(0, 2) || 'en'; 2 | 3 | export const translations = { 4 | en: 'Add to contacts', 5 | es: 'Agregar a contactos', 6 | fr: 'Ajouter aux contacts', 7 | de: 'Zu Kontakten hinzufügen', 8 | it: 'Aggiungi ai contatti', 9 | pt: 'Adicionar aos contatos', 10 | nl: 'Toevoegen aan contacten', 11 | sv: 'Lägg till i kontakter', 12 | ja: '連絡先に追加', 13 | ko: '연락처에 추가', 14 | zh: '添加到通讯录', 15 | ru: 'Добавить в контакты', 16 | ar: 'إضافة إلى جهات الاتصال', 17 | hi: 'संपर्कों में जोड़ें', 18 | bn: 'যোগ করুন কন্টাক্টে', 19 | he: 'הוסף לאנשי קשר', 20 | tr: 'Kişilere ekle', 21 | vi: 'Thêm vào danh bạ', 22 | pl: 'Dodaj do kontaktów', 23 | cs: 'Přidat do kontaktů', 24 | ro: 'Adaugă în contacte', 25 | hu: 'Hozzáadás a névjegyekhez', 26 | da: 'Tilføj til kontakter', 27 | }; 28 | -------------------------------------------------------------------------------- /src/lib/charts/PieChart.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 |
33 | 34 |
35 | 36 | 42 | -------------------------------------------------------------------------------- /src/lib/charts/StackedBarLineChart.svelte: -------------------------------------------------------------------------------- 1 | 54 | 55 |
56 | 57 |
58 | 59 | -------------------------------------------------------------------------------- /src/lib/forms/AlignSelect.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 |
11 | 16 | 17 |
18 | -------------------------------------------------------------------------------- /src/lib/forms/Button.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 12 | -------------------------------------------------------------------------------- /src/lib/forms/ColorInput.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 |
18 |
19 | 20 | 44 | -------------------------------------------------------------------------------- /src/lib/forms/CoordinatesInput.svelte: -------------------------------------------------------------------------------- 1 | 51 | 52 |
53 |
54 | 62 | 63 |
64 | 65 |
66 | 74 | 75 |
76 | 77 | 78 |
-------------------------------------------------------------------------------- /src/lib/forms/DateInput.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /src/lib/forms/EmailInput.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 | 27 | 28 |
29 | -------------------------------------------------------------------------------- /src/lib/forms/PasswordInput.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 |
19 | 29 | 30 |
31 | 41 |
42 | -------------------------------------------------------------------------------- /src/lib/forms/PhoneInput.svelte: -------------------------------------------------------------------------------- 1 | 16 | 17 |
18 |
19 | 24 | 25 |
26 | 27 |
28 | 36 | 37 |
38 | 39 |
40 | 48 | 49 |
50 |
51 | 52 | 57 | -------------------------------------------------------------------------------- /src/lib/forms/RangeInput.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | 15 | 24 |
25 | -------------------------------------------------------------------------------- /src/lib/forms/SearchInput.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 |
38 | 47 | 53 |
54 | -------------------------------------------------------------------------------- /src/lib/forms/SocialIconLink.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 | {#if link} 16 | {#if backgroundColor.length > 0 && fontColor.length > 0} 17 | 18 | 20 | 21 | {:else} 22 | 23 | 24 | 25 | {/if} 26 | {/if} 27 | -------------------------------------------------------------------------------- /src/lib/forms/SocialIconTextInput.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 13 | 14 | 15 |
16 | 17 | 18 |
19 |
20 | -------------------------------------------------------------------------------- /src/lib/forms/Switch.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
13 | 22 | 23 |
24 | 25 | 37 | -------------------------------------------------------------------------------- /src/lib/forms/TextInput.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 18 | 19 |
20 | -------------------------------------------------------------------------------- /src/lib/forms/TextareaInput.svelte: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 18 | 19 | The following HTML-tags are allowed: 'b', 'i', 'em', 'strong', 'a' 20 |
21 | -------------------------------------------------------------------------------- /src/lib/forms/TimeZoneSelect.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
12 | 17 | 18 |
19 | -------------------------------------------------------------------------------- /src/lib/forms/UploadFile.svelte: -------------------------------------------------------------------------------- 1 | 45 | 46 |
47 |
48 | onFileSelected(e)} 54 | type="file" 55 | /> 56 | Accepts .jpg, .jpeg, .png 57 |
58 | 59 | 60 | 61 | 62 | 63 |
64 | -------------------------------------------------------------------------------- /src/lib/layout/AdminMain.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | {#if isMounted} 23 |
24 | {#if screenWidth > 642} 25 | 26 | {:else} 27 | 28 | {/if} 29 | 30 |
31 | 32 |
33 |
34 | 35 |
36 |
37 |
38 |
39 | 40 | {/if} 41 | 42 | 53 | -------------------------------------------------------------------------------- /src/lib/layout/Divider.svelte: -------------------------------------------------------------------------------- 1 |
2 | 3 | 10 | -------------------------------------------------------------------------------- /src/lib/layout/Heading.svelte: -------------------------------------------------------------------------------- 1 | 8 |
9 | {#if tag === 'h1'} 10 |

11 | 12 |

13 | {:else if tag === 'h2'} 14 |

15 | 16 |

17 | {:else if tag === 'h3'} 18 |

19 | 20 |

21 | {:else} 22 |

23 | 24 |

25 | {/if} 26 |
27 | -------------------------------------------------------------------------------- /src/lib/layout/IconLink.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | 22 | -------------------------------------------------------------------------------- /src/lib/layout/NavbarTop.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
16 | -------------------------------------------------------------------------------- /src/lib/layout/Preview.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/lib/layout/menus/MenuDivider.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | -------------------------------------------------------------------------------- /src/lib/layout/menus/desktop/DesktopMenu.svelte: -------------------------------------------------------------------------------- 1 | 67 | 68 | 139 | 140 | 148 | -------------------------------------------------------------------------------- /src/lib/layout/menus/desktop/DesktopMenuItem.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 | {#if href} 37 | 48 | 49 | 50 | {:else} 51 | 62 | {/if} 63 | 64 | -------------------------------------------------------------------------------- /src/lib/layout/menus/mobile/MobileMenu.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 | 65 |
66 | 67 | 87 | -------------------------------------------------------------------------------- /src/lib/logo/Logo.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#if logo === "true"} 12 | 20 | 22 | 23 | {/if} 24 | 25 | {#if text === "true"} 26 | 34 | 36 | 37 | {/if} 38 | -------------------------------------------------------------------------------- /src/lib/maps/Map.svelte: -------------------------------------------------------------------------------- 1 | 55 | 56 |
57 | 58 | 65 | -------------------------------------------------------------------------------- /src/lib/nfc/Nfc.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 | {#if showOverlay} 51 | 75 | {/if} 76 | 77 | 80 | 81 | 114 | -------------------------------------------------------------------------------- /src/lib/pagination/Pagination.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 | 64 | -------------------------------------------------------------------------------- /src/lib/theme/ThemeToggle.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 | 41 | 42 | 58 | -------------------------------------------------------------------------------- /src/lib/vCard/BusinessCard.svelte: -------------------------------------------------------------------------------- 1 | 76 | 77 |
78 |
79 |
80 | 81 | {#if theme.display.logo} 82 | {#if theme?.logo?.format?.webp || logoPreview} 83 |
84 | {vCard.professional.company} 92 |
93 | {/if} 94 | {/if} 95 | 96 | 97 | 98 | {#if vCard?.avatar?.format?.webp || avatarPreview} 99 |
100 |
101 | {fullName} 108 |
109 |
110 | {/if} 111 | 112 |
113 |

{fullName}

115 |
116 |
117 | {vCard.person.pronouns} 118 |

{vCard.professional.role}

119 |

{@html vCard.professional.bio.replace(/(\r\n|\r|\n)/g, '
')}

120 |
121 | 122 | 156 | 157 | 158 | 159 | 160 | 274 | 275 | {#if theme.display.map && latitude.length !== 0 && longitude.length !== 0} 276 | 277 | 278 |
279 | 283 |
284 | {/if} 285 |
286 | 287 |
288 | 289 | {#if theme.display.vCardBtn} 290 |
291 | 297 | 298 | {addContactText} 299 | 300 |
301 | {/if} 302 |
303 | 304 | 358 | -------------------------------------------------------------------------------- /src/lib/vCard/DisplayPreview.svelte: -------------------------------------------------------------------------------- 1 | 27 | 28 |
29 | {#if screenWidth >= 992} 30 |
31 | 32 |
33 | {/if} 34 |
35 | 36 | 44 | -------------------------------------------------------------------------------- /src/lib/vCard/VCard.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 | 36 | -------------------------------------------------------------------------------- /src/lib/vCard/views/MobileView.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |
8 | 9 |
10 |
11 | 12 | 57 | -------------------------------------------------------------------------------- /src/lib/vCard/views/ProductionView.svelte: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | 7 | 25 | -------------------------------------------------------------------------------- /src/routes/+error.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 |
13 |
14 | {$page.error.message} 15 |
16 |
17 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 | 8 |
9 | -------------------------------------------------------------------------------- /src/routes/+page.server.js: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | 3 | export function load() { 4 | throw redirect(302, '/login'); 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/admin/+page.server.js: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import { PUBLIC_REST_API_URL } from '$env/static/public'; 3 | 4 | export const load = async ({ 5 | fetch, 6 | cookies, 7 | url, 8 | }) => { 9 | const { token } = JSON.parse(cookies.get('access')).data; 10 | const userId = url.searchParams.get('userId') || JSON.parse(cookies.get('user')).data.userId; 11 | 12 | const clicks = async () => { 13 | const options = { 14 | method: 'GET', 15 | headers: { 16 | 'Content-Type': 'application/json', 17 | Authorization: `Bearer ${token}`, 18 | }, 19 | }; 20 | 21 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/users/${userId}/statistics/clicks`, options); 22 | 23 | if (response.status === 404) { 24 | throw redirect(302, '/login'); 25 | } 26 | 27 | return response.json(); 28 | }; 29 | 30 | return { 31 | clicks: clicks(), 32 | }; 33 | }; 34 | -------------------------------------------------------------------------------- /src/routes/admin/+page.svelte: -------------------------------------------------------------------------------- 1 | 61 | 62 | 66 | 67 | 68 | Personal Dashboard 69 | 70 |
71 |
72 |
73 |
74 | 75 | 76 | Business Card 77 | 78 | 82 |
83 | 84 |
85 |
86 |
87 |
88 |
89 | 90 |
91 |
92 |
93 | 94 | 95 | QR 96 | 97 | 98 |
99 | 105 | 106 | 110 |
111 | 112 |
113 |
114 |
115 |
116 |
117 |
118 | 119 |
120 |
121 |
122 | 123 | 124 | Total clicks 125 | 126 |

{clicks.totalClicks}

127 |
128 |
129 |
130 |
131 | 132 |
133 |
134 |
135 |
136 | Total clicks by type 137 | 138 |
139 |
140 |
141 | 142 |
143 |
144 |
145 | Last 7 days of activity 146 | 147 |
148 |
149 |
150 |
151 |
152 | 153 | -------------------------------------------------------------------------------- /src/routes/admin/themes/+page.server.js: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import { PUBLIC_REST_API_URL } from '$env/static/public'; 3 | 4 | export const load = async ({ 5 | fetch, 6 | cookies, 7 | url, 8 | }) => { 9 | const { token } = JSON.parse(cookies.get('access')).data; 10 | 11 | const fetchThemes = async () => { 12 | const params = new URLSearchParams(url.search); 13 | const page = params.get('page'); 14 | const limit = params.get('limit') ?? 5; 15 | const search = params.get('search'); 16 | 17 | const options = { 18 | method: 'GET', 19 | headers: { 20 | 'Content-Type': 'application/json', 21 | Authorization: `Bearer ${token}`, 22 | }, 23 | }; 24 | 25 | const apiUrl = new URL(`${PUBLIC_REST_API_URL}/api/v1/themes`); 26 | 27 | if (page) { 28 | apiUrl.searchParams.append('page', page); 29 | } 30 | if (limit) { 31 | apiUrl.searchParams.append('limit', limit); 32 | } 33 | if (search) { 34 | apiUrl.searchParams.append('search', search); 35 | } 36 | 37 | const response = await fetch(apiUrl.toString(), options); 38 | 39 | if (response.status === 404) { 40 | throw redirect(302, '/login'); 41 | } 42 | 43 | if (response.status === 403) { 44 | throw redirect(302, '/admin'); 45 | } 46 | 47 | return response.json(); 48 | }; 49 | 50 | return { 51 | themes: fetchThemes(), 52 | }; 53 | }; 54 | 55 | export const actions = { 56 | view: async ({ 57 | fetch, 58 | request, 59 | cookies, 60 | }) => { 61 | const { token } = JSON.parse(cookies.get('access')).data; 62 | const formData = await request.formData(); 63 | const data = Object.fromEntries(Array.from(formData.entries())); 64 | 65 | const options = { 66 | method: 'PUT', 67 | headers: { 68 | 'Content-Type': 'application/json', 69 | Authorization: `Bearer ${token}`, 70 | }, 71 | body: JSON.stringify(data), 72 | }; 73 | 74 | try { 75 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/themes`, options); 76 | 77 | if (response.ok) { 78 | return { success: true }; 79 | } 80 | return { success: false }; 81 | } catch (err) { 82 | return { success: false }; 83 | } 84 | }, 85 | 86 | delete: async ({ 87 | fetch, 88 | request, 89 | cookies, 90 | }) => { 91 | const { token } = JSON.parse(cookies.get('access')).data; 92 | const formData = await request.formData(); 93 | 94 | const options = { 95 | method: 'DELETE', 96 | headers: { 97 | 'Content-Type': 'application/json', 98 | Authorization: `Bearer ${token}`, 99 | }, 100 | body: JSON.stringify({ 101 | themeId: formData.get('themeId'), 102 | }), 103 | }; 104 | 105 | try { 106 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/themes/${formData.get('themeId')}`, options); 107 | 108 | if (response.status === 400) { 109 | const data = await response.json(); 110 | return { 111 | success: false, 112 | ...data, 113 | }; 114 | } 115 | 116 | if (response.ok) { 117 | return { success: true }; 118 | } 119 | 120 | return { success: false }; 121 | } catch (err) { 122 | return { success: false }; 123 | } 124 | }, 125 | 126 | createTheme: async ({ 127 | fetch, 128 | request, 129 | cookies, 130 | }) => { 131 | const { token } = JSON.parse(cookies.get('access')).data; 132 | const formData = await request.formData(); 133 | 134 | const options = { 135 | method: 'POST', 136 | headers: { 137 | 'Content-Type': 'application/json', 138 | Authorization: `Bearer ${token}`, 139 | }, 140 | body: JSON.stringify({ 141 | name: formData.get('name'), 142 | }), 143 | }; 144 | 145 | try { 146 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/themes`, options); 147 | 148 | if (response.ok) { 149 | return { success: true }; 150 | } 151 | return { success: false }; 152 | } catch (err) { 153 | return { success: false }; 154 | } 155 | }, 156 | }; 157 | -------------------------------------------------------------------------------- /src/routes/admin/themes/+page.svelte: -------------------------------------------------------------------------------- 1 | 83 | 84 | 88 | 89 | 90 | Themes 91 | 92 | 93 | 94 | 123 | 124 |
125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | {#each data.themes.data as theme} 134 | 135 | 136 | 207 | 208 | {/each} 209 | 210 |
NameActions
{theme.name} 137 | 142 |
143 | 144 |
145 |
146 | 147 |
148 | 154 | 164 |
165 | 166 | 206 |
211 |
212 | 213 | 214 | 215 | 226 |
227 | 228 | 266 | -------------------------------------------------------------------------------- /src/routes/admin/themes/[id]/+page.server.js: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import { PUBLIC_REST_API_URL } from '$env/static/public'; 3 | 4 | export const load = async ({ 5 | fetch, 6 | cookies, 7 | params, 8 | }) => { 9 | const { 10 | role, 11 | userId, 12 | } = JSON.parse(cookies.get('user')).data; 13 | const { token } = JSON.parse(cookies.get('access')).data; 14 | 15 | const themeId = params.id; 16 | 17 | if (!['admin', 'editor'].includes(role)) { 18 | throw redirect(302, '/admin'); 19 | } 20 | 21 | const fetchVcard = async () => { 22 | const options = { 23 | method: 'GET', 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | Authorization: `Bearer ${token}`, 27 | }, 28 | }; 29 | 30 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/users/${userId}/vcards`, options); 31 | return response.json(); 32 | }; 33 | 34 | const fetchTheme = async () => { 35 | const options = { 36 | method: 'GET', 37 | headers: { 38 | 'Content-Type': 'application/json', 39 | Authorization: `Bearer ${token}`, 40 | }, 41 | }; 42 | 43 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/themes/${themeId}`, options); 44 | 45 | return response.json(); 46 | }; 47 | 48 | return { 49 | vCards: fetchVcard(), 50 | theme: fetchTheme(), 51 | }; 52 | }; 53 | 54 | export const actions = { 55 | save: async ({ 56 | fetch, 57 | request, 58 | cookies, 59 | params, 60 | }) => { 61 | const { token } = JSON.parse(cookies.get('access')).data; 62 | const themeId = params.id; 63 | const formData = await request.formData(); 64 | 65 | const data = { 66 | color: { 67 | font: { 68 | primary: formData.get('fontColor'), 69 | secondary: formData.get('secondaryFontColor'), 70 | }, 71 | background: formData.get('backgroundColor'), 72 | socialIcons: { 73 | font: formData.get('socialIconFontColor'), 74 | background: formData.get('socialIconBackgroundColor'), 75 | }, 76 | contactIcons: { 77 | font: formData.get('contactIconFontColor'), 78 | background: formData.get('contactIconBackgroundColor'), 79 | }, 80 | vCardBtn: { 81 | font: formData.get('btnFontColor'), 82 | background: formData.get('btnBackgroundColor'), 83 | }, 84 | }, 85 | display: { 86 | logo: formData.get('displayLogo') === 'on', 87 | phone: formData.get('displayPhone') === 'on', 88 | sms: formData.get('displaySms') === 'on', 89 | email: formData.get('displayEmail') === 'on', 90 | web: formData.get('displayWeb') === 'on', 91 | address: formData.get('displayAddress') === 'on', 92 | map: formData.get('displayMap') === 'on', 93 | vCardBtn: formData.get('displayContactBtn') === 'on', 94 | }, 95 | align: { 96 | logo: formData.get('alignLogo'), 97 | avatar: formData.get('alignAvatar'), 98 | heading: formData.get('alignHeading'), 99 | bio: formData.get('alignBio'), 100 | socialIcons: formData.get('alignSocialIcons'), 101 | }, 102 | logo: { 103 | size: { 104 | height: formData.get('logoHeight'), 105 | }, 106 | }, 107 | }; 108 | 109 | const options = { 110 | method: 'PUT', 111 | headers: { 112 | 'Content-Type': 'application/json', 113 | Authorization: `Bearer ${token}`, 114 | }, 115 | body: JSON.stringify(data), 116 | }; 117 | 118 | try { 119 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/themes/${themeId}`, options); 120 | 121 | if (response.ok) { 122 | return { success: true }; 123 | } 124 | 125 | return { success: false }; 126 | } catch (err) { 127 | return { success: false }; 128 | } 129 | }, 130 | 131 | uploadLogo: async ({ 132 | fetch, 133 | request, 134 | cookies, 135 | }) => { 136 | const { token } = JSON.parse(cookies.get('access')).data; 137 | const { themeId } = JSON.parse(cookies.get('user')).data; 138 | const formData = await request.formData(); 139 | 140 | const options = { 141 | method: 'POST', 142 | headers: { 143 | Authorization: `Bearer ${token}`, 144 | }, 145 | body: formData, 146 | }; 147 | 148 | try { 149 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/themes/${themeId}/images`, options); 150 | 151 | if (response.ok) { 152 | return { success: true }; 153 | } 154 | return { success: false }; 155 | } catch (err) { 156 | return { success: false }; 157 | } 158 | }, 159 | 160 | createTheme: async ({ 161 | fetch, 162 | request, 163 | cookies, 164 | }) => { 165 | const { token } = JSON.parse(cookies.get('access')).data; 166 | const formData = await request.formData(); 167 | 168 | const options = { 169 | method: 'POST', 170 | headers: { 171 | 'Content-Type': 'application/json', 172 | Authorization: `Bearer ${token}`, 173 | }, 174 | body: JSON.stringify({ 175 | name: formData.get('name'), 176 | }), 177 | }; 178 | 179 | try { 180 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/themes`, options); 181 | 182 | if (response.ok) { 183 | return { success: true }; 184 | } 185 | return { success: false }; 186 | } catch (err) { 187 | return { success: false }; 188 | } 189 | }, 190 | }; 191 | -------------------------------------------------------------------------------- /src/routes/admin/themes/[id]/+page.svelte: -------------------------------------------------------------------------------- 1 | 43 | 44 | 48 | 49 | 50 | Edit theme 51 |
52 |
53 |
54 | Colors 55 | 61 | 67 | 73 | 79 | 85 | 91 | 97 | 103 | 109 | Align content 110 |
200 |
201 | 202 |
203 | -------------------------------------------------------------------------------- /src/routes/admin/users/+page.server.js: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import { PUBLIC_REST_API_URL } from '$env/static/public'; 3 | 4 | export const load = async ({ 5 | fetch, 6 | cookies, 7 | url, 8 | }) => { 9 | const { token } = JSON.parse(cookies.get('access')).data; 10 | 11 | const fetchThemes = async () => { 12 | const options = { 13 | method: 'GET', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | Authorization: `Bearer ${token}`, 17 | }, 18 | }; 19 | 20 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/themes`, options); 21 | 22 | return response.json(); 23 | }; 24 | 25 | const fetchUsers = async () => { 26 | const params = new URLSearchParams(url.search); 27 | const page = params.get('page'); 28 | const limit = params.get('limit') ?? 5; 29 | const search = params.get('search'); 30 | 31 | const options = { 32 | method: 'GET', 33 | headers: { 34 | 'Content-Type': 'application/json', 35 | Authorization: `Bearer ${token}`, 36 | }, 37 | }; 38 | 39 | const apiUrl = new URL(`${PUBLIC_REST_API_URL}/api/v1/users`); 40 | 41 | if (page) { 42 | apiUrl.searchParams.append('page', page); 43 | } 44 | if (limit) { 45 | apiUrl.searchParams.append('limit', limit); 46 | } 47 | if (search) { 48 | apiUrl.searchParams.append('search', search); 49 | } 50 | 51 | const response = await fetch(apiUrl.toString(), options); 52 | 53 | if (response.status === 404) { 54 | throw redirect(302, '/login'); 55 | } 56 | 57 | if (response.status === 403) { 58 | throw redirect(302, '/admin'); 59 | } 60 | 61 | return response.json(); 62 | }; 63 | 64 | return { 65 | themes: fetchThemes(), 66 | users: fetchUsers(), 67 | }; 68 | }; 69 | 70 | export const actions = { 71 | view: async ({ 72 | request, 73 | cookies, 74 | }) => { 75 | const { token } = JSON.parse(cookies.get('access')).data; 76 | const formData = await request.formData(); 77 | const data = Object.fromEntries(Array.from(formData.entries())); 78 | 79 | const options = { 80 | method: 'PUT', 81 | headers: { 82 | 'Content-Type': 'application/json', 83 | Authorization: `Bearer ${token}`, 84 | }, 85 | body: JSON.stringify(data), 86 | }; 87 | 88 | try { 89 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/users`, options); 90 | 91 | if (response.ok) { 92 | return { success: true }; 93 | } 94 | return { success: false }; 95 | } catch (err) { 96 | return { success: false }; 97 | } 98 | }, 99 | 100 | send: async ({ 101 | request, 102 | cookies, 103 | }) => { 104 | const { token } = JSON.parse(cookies.get('access')).data; 105 | const formData = await request.formData(); 106 | const options = { 107 | method: 'POST', 108 | headers: { 109 | 'Content-Type': 'application/json', 110 | Authorization: `Bearer ${token}`, 111 | }, 112 | body: JSON.stringify({ 113 | userId: formData.get('userId'), 114 | }), 115 | }; 116 | 117 | try { 118 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/auth/recover`, options); 119 | if (response.ok) { 120 | return { success: true }; 121 | } 122 | return { success: false }; 123 | } catch (err) { 124 | return { success: false }; 125 | } 126 | }, 127 | 128 | delete: async ({ 129 | request, 130 | cookies, 131 | }) => { 132 | const { token } = JSON.parse(cookies.get('access')).data; 133 | const formData = await request.formData(); 134 | 135 | const options = { 136 | method: 'DELETE', 137 | headers: { 138 | 'Content-Type': 'application/json', 139 | Authorization: `Bearer ${token}`, 140 | }, 141 | body: JSON.stringify({ 142 | userId: formData.get('userId'), 143 | }), 144 | }; 145 | 146 | try { 147 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/users/${formData.get('userId')}`, options); 148 | 149 | if (response.ok) { 150 | return { success: true }; 151 | } 152 | return { success: false }; 153 | } catch (err) { 154 | return { success: false }; 155 | } 156 | }, 157 | 158 | updateUser: async ({ 159 | fetch, 160 | request, 161 | cookies, 162 | }) => { 163 | const { token } = JSON.parse(cookies.get('access')).data; 164 | const formData = await request.formData(); 165 | 166 | const options = { 167 | method: 'PUT', 168 | headers: { 169 | 'Content-Type': 'application/json', 170 | Authorization: `Bearer ${token}`, 171 | }, 172 | body: JSON.stringify({ 173 | themeId: formData.get('themeId'), 174 | email: formData.get('email'), 175 | name: formData.get('name'), 176 | role: formData.get('role'), 177 | }), 178 | }; 179 | 180 | try { 181 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/users/${formData.get('userId')}`, options); 182 | 183 | // Update the cookie. 184 | await cookies.set('user', JSON.stringify(await response.json()), { 185 | path: '/', 186 | maxAge: 3600 * 60 * 60 * 24, // 1 day 187 | secure: true, 188 | sameSite: 'strict', 189 | httpOnly: false, 190 | }); 191 | 192 | if (response.ok) { 193 | return { success: true }; 194 | } 195 | return { success: false }; 196 | } catch (err) { 197 | return { success: false }; 198 | } 199 | }, 200 | 201 | create: async ({ 202 | request, 203 | cookies, 204 | }) => { 205 | const { token } = JSON.parse(cookies.get('access')).data; 206 | const formData = await request.formData(); 207 | 208 | const options = { 209 | method: 'POST', 210 | headers: { 211 | 'Content-Type': 'application/json', 212 | Authorization: `Bearer ${token}`, 213 | }, 214 | body: JSON.stringify({ 215 | themeId: formData.get('themeId'), 216 | email: formData.get('email'), 217 | name: formData.get('name'), 218 | role: formData.get('role'), 219 | sendMail: Boolean(formData.get('sendMail')), 220 | }), 221 | }; 222 | 223 | try { 224 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/auth/signup`, options); 225 | 226 | if (response.ok) { 227 | return { success: true }; 228 | } 229 | return { success: false }; 230 | } catch (err) { 231 | return { success: false }; 232 | } 233 | }, 234 | }; 235 | -------------------------------------------------------------------------------- /src/routes/admin/users/+page.svelte: -------------------------------------------------------------------------------- 1 | 110 | 111 | 115 | 116 | 117 | Users 118 | 119 | 120 | 121 | 207 | 208 |
209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | {#each data.users.data as user} 220 | 221 | 222 | 223 | 224 | 423 | 424 | {/each} 425 | 426 |
NameEmailRoleActions
{user.name}{user.email}{user.role} 225 | 231 |
232 | 233 |
234 |
235 | 236 | 240 |
241 | 242 |
243 |
244 | 245 |
246 | 252 | 291 |
292 | 293 | 333 | 334 | 422 |
427 |
428 | 429 | 430 | 431 | 442 |
443 | 444 | 482 | -------------------------------------------------------------------------------- /src/routes/admin/vcard/+page.server.js: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import { PUBLIC_REST_API_URL } from '$env/static/public'; 3 | 4 | export const load = async ({ 5 | fetch, 6 | cookies, 7 | url, 8 | }) => { 9 | const { token } = JSON.parse(cookies.get('access')).data; 10 | 11 | const userId = url.searchParams.get('userId') || JSON.parse(cookies.get('user')).data.userId; 12 | const themeId = url.searchParams.get('themeId') || JSON.parse(cookies.get('user')).data.themeId; 13 | 14 | const fetchVcard = async () => { 15 | const options = { 16 | method: 'GET', 17 | headers: { 18 | 'Content-Type': 'application/json', 19 | Authorization: `Bearer ${token}`, 20 | }, 21 | }; 22 | 23 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/users/${userId}/vcards`, options); 24 | 25 | if (response.status === 404) { 26 | throw redirect(302, '/login'); 27 | } 28 | 29 | return response.json(); 30 | }; 31 | 32 | const fetchTheme = async () => { 33 | const options = { 34 | method: 'GET', 35 | headers: { 36 | 'Content-Type': 'application/json', 37 | Authorization: `Bearer ${token}`, 38 | }, 39 | }; 40 | 41 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/themes/${themeId}`, options); 42 | 43 | return response.json(); 44 | }; 45 | 46 | return { 47 | vCard: fetchVcard(), 48 | theme: fetchTheme(), 49 | }; 50 | }; 51 | 52 | export const actions = { 53 | save: async ({ 54 | fetch, 55 | request, 56 | cookies, 57 | url, 58 | }) => { 59 | const { token } = JSON.parse(cookies.get('access')).data; 60 | const formData = await request.formData(); 61 | 62 | const vCard = { 63 | person: { 64 | firstName: formData.get('firstName'), 65 | middleName: formData.get('middleName'), 66 | lastName: formData.get('lastName'), 67 | suffix: formData.get('suffix'), 68 | birthday: formData.get('birthday'), 69 | pronouns: formData.get('pronouns'), 70 | }, 71 | professional: { 72 | title: formData.get('title'), 73 | company: formData.get('company'), 74 | role: formData.get('role'), 75 | bio: formData.get('bio'), 76 | }, 77 | contact: { 78 | phone: { 79 | number: formData.get('number'), 80 | countryCode: formData.get('countryCode'), 81 | extension: formData.get('extension'), 82 | }, 83 | email: formData.get('email'), 84 | web: formData.get('web'), 85 | file: { 86 | url: formData.get('fileUrl'), 87 | name: formData.get('fileName'), 88 | }, 89 | }, 90 | location: { 91 | street: formData.get('street'), 92 | storey: formData.get('storey'), 93 | city: formData.get('city'), 94 | state: formData.get('state'), 95 | postalCode: formData.get('postalCode'), 96 | country: formData.get('country'), 97 | timeZone: formData.get('timeZone'), 98 | coordinates: { 99 | latitude: +formData.get('latitude'), 100 | longitude: +formData.get('longitude'), 101 | }, 102 | }, 103 | socialMedia: { 104 | twitter: formData.get('twitter'), 105 | linkedin: formData.get('linkedin'), 106 | facebook: formData.get('facebook'), 107 | instagram: formData.get('instagram'), 108 | pinterest: formData.get('pinterest'), 109 | github: formData.get('github'), 110 | }, 111 | }; 112 | 113 | const userId = url.searchParams.get('userId') || JSON.parse(cookies.get('user')).data.userId; 114 | 115 | const options = { 116 | method: 'PUT', 117 | headers: { 118 | 'Content-Type': 'application/json', 119 | Authorization: `Bearer ${token}`, 120 | }, 121 | body: JSON.stringify(vCard), 122 | }; 123 | 124 | try { 125 | const response = await fetch( 126 | `${PUBLIC_REST_API_URL}/api/v1/users/${userId}/vcards`, 127 | options, 128 | ); 129 | 130 | if (response.ok) { 131 | return { success: true }; 132 | } 133 | return { success: false }; 134 | } catch (err) { 135 | return { success: false }; 136 | } 137 | }, 138 | 139 | uploadLogo: async ({ 140 | fetch, 141 | request, 142 | cookies, 143 | }) => { 144 | const { token } = JSON.parse(cookies.get('access')).data; 145 | const { userId } = JSON.parse(cookies.get('user')).data; 146 | const formData = await request.formData(); 147 | 148 | const options = { 149 | method: 'POST', 150 | headers: { 151 | Authorization: `Bearer ${token}`, 152 | }, 153 | body: formData, 154 | }; 155 | 156 | try { 157 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/users/${userId}/images`, options); 158 | 159 | if (response.ok) { 160 | return { success: true }; 161 | } 162 | return { success: false }; 163 | } catch (err) { 164 | return { success: false }; 165 | } 166 | }, 167 | }; 168 | -------------------------------------------------------------------------------- /src/routes/admin/vcard/+page.svelte: -------------------------------------------------------------------------------- 1 | 58 | 59 | 63 | 64 | 65 | Edit vCard 66 |
67 |
68 |
69 | 71 | 73 | 74 | 75 | 76 | 77 | 79 | 81 | 82 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 101 | 102 | 103 |
104 | 106 | 107 |
108 | 109 | 110 | 117 | 124 | 131 | 138 | 145 | 152 | 153 | 154 | 155 | Images 156 | Upload avatar 157 | 158 |
159 | 160 | 161 |
162 |
163 | -------------------------------------------------------------------------------- /src/routes/login/+page.server.js: -------------------------------------------------------------------------------- 1 | import { PUBLIC_REST_API_URL } from '$env/static/public'; 2 | import { displayWarning } from '../../js/toast.js'; 3 | 4 | export const actions = { 5 | login: async ({ 6 | fetch, 7 | request, 8 | cookies, 9 | }) => { 10 | const formData = await request.formData(); 11 | 12 | const options = { 13 | method: 'POST', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | body: JSON.stringify({ 18 | email: formData.get('email'), 19 | password: formData.get('password'), 20 | }), 21 | }; 22 | 23 | try { 24 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/auth/login`, options); 25 | 26 | if (response.ok) { 27 | const user = await response.json(); 28 | 29 | await cookies.set('access', JSON.stringify({ 30 | data: { 31 | token: user.data.token, 32 | }, 33 | }), { 34 | path: '/', 35 | maxAge: 3600 * 60 * 60 * 24, // 1 day 36 | secure: true, 37 | sameSite: 'strict', 38 | httpOnly: true, 39 | }); 40 | 41 | delete user.data.token; 42 | await cookies.set('user', JSON.stringify(user), { 43 | path: '/', 44 | maxAge: 3600 * 60 * 60 * 24, // 1 day 45 | secure: true, 46 | sameSite: 'strict', 47 | httpOnly: false, 48 | }); 49 | 50 | return { success: true }; 51 | } 52 | 53 | displayWarning('Wrong credentials'); 54 | return { success: false }; 55 | } catch 56 | (err) { 57 | displayWarning('Error during login'); 58 | return { success: false }; 59 | } 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /src/routes/login/+page.svelte: -------------------------------------------------------------------------------- 1 | 38 | 39 | 43 | 44 |
45 |
46 |
47 |
48 |
49 | 50 |
51 | 52 |
53 | Login 54 |
55 | 56 |
57 |
58 | 59 |
60 | 62 |
63 |
64 | 66 |
67 | 68 | 71 |
72 |
73 |
74 |
75 |
76 |
77 | 78 | 79 | 86 | -------------------------------------------------------------------------------- /src/routes/p/[id]/t/[themeId]/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect } from '@sveltejs/kit'; 2 | import { PUBLIC_REST_API_URL } from '$env/static/public'; 3 | 4 | export const prerender = 'auto'; 5 | 6 | export const load = async ({ 7 | fetch, 8 | params, 9 | url, 10 | }) => { 11 | const fetchVcard = async () => { 12 | const options = { 13 | method: 'GET', 14 | headers: { 15 | 'Content-Type': 'application/json', 16 | }, 17 | }; 18 | 19 | const response = await fetch( 20 | `${PUBLIC_REST_API_URL}/api/v1/users/${params.id}/vcards`, 21 | options, 22 | ); 23 | 24 | if (response.status === 404) { 25 | throw redirect(302, '/404'); 26 | } 27 | 28 | return response.json(); 29 | }; 30 | 31 | const fetchTheme = async () => { 32 | const options = { 33 | method: 'GET', 34 | headers: { 35 | 'Content-Type': 'application/json', 36 | }, 37 | }; 38 | 39 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/themes/${params.themeId}`, options); 40 | 41 | return response.json(); 42 | }; 43 | 44 | const fetchClicks = async () => { 45 | const data = { source: url.searchParams.get('source') }; 46 | 47 | const options = { 48 | method: 'POST', 49 | headers: { 50 | 'Content-Type': 'application/json', 51 | }, 52 | body: JSON.stringify(data), 53 | }; 54 | 55 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/users/${params.id}/statistics/clicks`, options); 56 | 57 | return response.json(); 58 | }; 59 | 60 | await fetchClicks(); 61 | 62 | return { 63 | vCard: fetchVcard(), 64 | theme: fetchTheme(), 65 | }; 66 | }; 67 | -------------------------------------------------------------------------------- /src/routes/p/[id]/t/[themeId]/+page.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 35 | 36 | 37 | 38 | 39 | 40 | 85 | -------------------------------------------------------------------------------- /src/routes/qr/[id]/t/[themeId]/+page.js: -------------------------------------------------------------------------------- 1 | import QRCode from 'qrcode-svg'; 2 | import { dev } from '$app/environment'; 3 | import { PUBLIC_BASE_URL } from '$env/static/public'; 4 | 5 | export const csr = dev; 6 | 7 | export const prerender = 'auto'; 8 | 9 | export const load = async ({ 10 | params, 11 | }) => { 12 | const profileUrl = `${PUBLIC_BASE_URL}/p/${params.id}/t/${params.themeId}`; 13 | 14 | return { 15 | qrCode: new QRCode({ 16 | content: `${profileUrl}?source=qr`, 17 | join: true, 18 | ecl: 'M', 19 | width: 300, 20 | height: 300, 21 | margin: 20, 22 | color: '#212529', 23 | background: '#fff', 24 | }).svg(), 25 | }; 26 | }; 27 | -------------------------------------------------------------------------------- /src/routes/qr/[id]/t/[themeId]/+page.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 11 | 12 |
13 | {@html data.qrCode} 14 |
15 | 16 | 25 | -------------------------------------------------------------------------------- /src/routes/reset/+page.server.js: -------------------------------------------------------------------------------- 1 | import { PUBLIC_REST_API_URL } from '$env/static/public'; 2 | 3 | export const load = async ({ url }) => { 4 | const token = url.searchParams.get('token'); 5 | const email = url.searchParams.get('email'); 6 | 7 | return { 8 | email, 9 | token, 10 | }; 11 | }; 12 | 13 | export const actions = { 14 | save: async ({ 15 | fetch, 16 | request, 17 | }) => { 18 | const formData = await request.formData(); 19 | 20 | const body = { 21 | token: formData.get('token'), 22 | password: formData.get('password'), 23 | email: formData.get('email'), 24 | }; 25 | 26 | const options = { 27 | method: 'PUT', 28 | headers: { 29 | 'Content-Type': 'application/json', 30 | }, 31 | body: JSON.stringify(body), 32 | }; 33 | 34 | try { 35 | const response = await fetch(`${PUBLIC_REST_API_URL}/api/v1/auth/reset`, options); 36 | 37 | if (response.ok) { 38 | return { success: true }; 39 | } 40 | 41 | return { success: false }; 42 | } catch (err) { 43 | return { success: false }; 44 | } 45 | }, 46 | }; 47 | -------------------------------------------------------------------------------- /src/routes/reset/+page.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 | 39 | 40 |
41 |
42 |
43 |
44 |
45 | Set your new password 46 |
47 | 48 |
49 |
50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 |
58 |
59 |
60 |
61 |
62 | 63 | 64 | -------------------------------------------------------------------------------- /src/variables.scss: -------------------------------------------------------------------------------- 1 | /* Variables and mixins declared here will be available in all other SCSS files */ 2 | @import 'bootstrap/scss/functions'; 3 | @import 'bootstrap/scss/variables'; 4 | @import 'bootstrap/scss/maps'; 5 | @import 'bootstrap/scss/mixins'; 6 | @import 'bootstrap/scss/utilities'; 7 | -------------------------------------------------------------------------------- /static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CardMesh/web-app/4fc81fdeabef60ec1e567fc2d968b84f42fc5a78/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CardMesh/web-app/4fc81fdeabef60ec1e567fc2d968b84f42fc5a78/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CardMesh/web-app/4fc81fdeabef60ec1e567fc2d968b84f42fc5a78/static/apple-touch-icon.png -------------------------------------------------------------------------------- /static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #74aa9c 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CardMesh/web-app/4fc81fdeabef60ec1e567fc2d968b84f42fc5a78/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-194x194.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CardMesh/web-app/4fc81fdeabef60ec1e567fc2d968b84f42fc5a78/static/favicon-194x194.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CardMesh/web-app/4fc81fdeabef60ec1e567fc2d968b84f42fc5a78/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CardMesh/web-app/4fc81fdeabef60ec1e567fc2d968b84f42fc5a78/static/favicon.ico -------------------------------------------------------------------------------- /static/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CardMesh/web-app/4fc81fdeabef60ec1e567fc2d968b84f42fc5a78/static/mstile-150x150.png -------------------------------------------------------------------------------- /static/robots.txt: -------------------------------------------------------------------------------- 1 | User-agent: * 2 | Disallow: 3 | -------------------------------------------------------------------------------- /static/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.14, written by Peter Selinger 2001-2017 9 | 10 | 12 | 136 | 137 | 138 | -------------------------------------------------------------------------------- /static/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CardMesh", 3 | "short_name": "CardMesh", 4 | "icons": [ 5 | { 6 | "src": "/android-chrome-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png" 9 | }, 10 | { 11 | "src": "/android-chrome-512x512.png", 12 | "sizes": "512x512", 13 | "type": "image/png" 14 | } 15 | ], 16 | "theme_color": "#74aa9c", 17 | "background_color": "#74aa9c", 18 | "display": "standalone" 19 | } 20 | -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import preprocess from 'svelte-preprocess'; 2 | import adapter from '@sveltejs/adapter-vercel'; 3 | 4 | const config = { 5 | kit: { 6 | adapter: adapter(), 7 | }, 8 | 9 | preprocess: [ 10 | preprocess({}), 11 | ], 12 | }; 13 | 14 | export default config; 15 | -------------------------------------------------------------------------------- /tests/test.js: -------------------------------------------------------------------------------- 1 | import { expect, test } from '@playwright/test'; 2 | 3 | test('has title', async ({ page }) => { 4 | await page.goto('/login'); 5 | 6 | // Expect a title "to contain" a substring. 7 | await expect(page) 8 | .toHaveTitle('Login'); 9 | }); 10 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite'; 2 | import { defineConfig } from 'vitest/config'; 3 | 4 | export default defineConfig({ 5 | plugins: [sveltekit()], 6 | test: { 7 | include: ['src/**/*.{test,spec}.{js,ts}'], 8 | }, 9 | }); 10 | --------------------------------------------------------------------------------