Compare commits

..

No commits in common. "main" and "migrate-to-sveltekit" have entirely different histories.

104 changed files with 4833 additions and 4193 deletions

View file

@ -13,24 +13,24 @@ jobs:
- name: Checkout code
uses: actions/checkout@v2
- name: Set up SSH
run: |
mkdir -p ~/.ssh/
echo "${{ secrets.SSH_KEY }}" > ./deploy.key
chmod 600 ./deploy.key
echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
- name: Use Node.js 19
uses: actions/setup-node@v4
with:
node-version: 19
- name: Create env file
run: |
touch .env
echo EMAIL_ACCESS_KEY=${{ secrets.EMAIL_ACCESS_KEY }} >> .env
echo EMAIL_BASE_URL=${{ secrets.EMAIL_BASE_URL }} >> .env
- name: Verify .env file creation
run: cat .env
env:
SSH_PRIVATE_KEY: ${{secrets.SSH_KEY}}
SSH_KNOWN_HOSTS: ${{secrets.SSH_KNOWN_HOSTS}}
- name: Install PM2
run: npm i -g pm2
- name: Add Deploy Key to SSH
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_KEY }}" >> ./deploy.key
sudo chmod 600 ./deploy.key
echo "${{ secrets.SSH_KNOWN_HOSTS}}" > ~/.ssh/known_hosts
- name: Deploy
run: env $(cat .env | grep -v \"#\" | xargs) pm2 deploy ecosystem.config.cjs production
run: pm2 deploy ecosystem.config.cjs production

1
.gitignore vendored
View file

@ -8,4 +8,3 @@ node_modules
!.env.example
vite.config.js.timestamp-*
vite.config.ts.timestamp-*
deploy.key

View file

@ -1,73 +1,38 @@
# 🧵 Embroidery Viewer
# sv
![Logo](/logo.webp)
Everything you need to build a Svelte project, powered by [`sv`](https://github.com/sveltejs/cli).
![Deploy workflow status](https://git.leomurca.xyz/leomurca/embroidery-viewer/actions/workflows/deploy.yml/badge.svg)
## Creating a project
**The simplest way to preview embroidery files — instantly, in your browser.**
If you're seeing this, you've probably already done this step. Congrats!
👉 **Try it now:** https://embroideryviewer.xyz
```bash
# create a new project in the current directory
npx sv create
![Demo](/demo.gif)
# create a new project in my-app
npx sv create my-app
```
<a href="https://buymeacoffee.com/embroideryviewerxyz">
<img src="docs/yellow-button.png" width="200" alt="Alt Text">
</a>
## Developing
---
Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
## ✨ Why Embroidery Viewer?
```bash
npm run dev
Working with embroidery files shouldnt require heavy, expensive software.
# or start the server and open the app in a new browser tab
npm run dev -- --open
```
Embroidery Viewer was built to solve a simple problem:
## Building
> _“I just want to quickly see my design.”_
To create a production version of your app:
No installs. No friction. Just drag, drop, and view.
```bash
npm run build
```
---
You can preview the production build with `npm run preview`.
## 🚀 Features
- ⚡ **Instant preview** — open files in seconds
- 🔒 **Private by design** — everything runs in your browser
- 🧵 **Multiple formats supported**
- 🖥️ **Works on any device** (desktop, tablet, mobile)
- 📂 **Batch-friendly** — view multiple files in sequence
---
## 📁 Supported Formats
- `.pes`
- `.dst`
- `.pec`
- `.jef`
- `.exp`
---
## 🧠 How it works
All processing happens **client-side** using modern web technologies.
Your files are never uploaded to any server.
---
## 💡 Inspiration
Inspired by:
https://github.com/redteam316/html5-embroidery.git
---
## ❤️ Support the project
If this tool helped you, consider supporting its development:
- Share it with others
- Give feedback
- Or contribute to the project
---
> To deploy your app, you may need to install an [adapter](https://svelte.dev/docs/kit/adapters) for your target environment.

BIN
demo.gif

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

View file

@ -1,7 +1,7 @@
module.exports = {
apps: [
{
name: 'embroidery-viewer-prod',
name: 'embroidery-viewer',
script: './build/index.js',
time: true,
instances: 1,
@ -10,8 +10,6 @@ module.exports = {
watch: false,
max_memory_restart: '1G',
env: {
PORT: 7281,
PUBLIC_APP_ENV: 'production',
NODE_ENV: 'production',
},
},
@ -24,13 +22,11 @@ module.exports = {
ref: 'origin/main',
repo: 'git@git.leomurca.xyz:leomurca/embroidery-viewer.git',
path: '/home/deployer/embroidery-viewer',
'pre-deploy':
'rm -rf node_modules build .svelte-kit && npm ci && PUBLIC_APP_ENV=production npm run build',
'pre-deploy': 'rm package-lock.json && npm i',
'post-deploy':
'pm2 startOrReload ecosystem.config.cjs --only embroidery-viewer-prod --env production && pm2 save',
'npm run build && pm2 reload ecosystem.config.cjs --only acelera-alagoas-prod --env production && pm2 save',
env: {
PORT: 7281,
PUBLIC_APP_ENV: 'production',
PORT: 7017,
NODE_ENV: 'production',
},
},

BIN
logo.webp

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

2509
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "embroidery-viewer",
"private": true,
"version": "3.0.3",
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "vite dev",
@ -14,24 +14,23 @@
"lint": "prettier --check . && eslint ."
},
"devDependencies": {
"@eslint/compat": "^2.0.5",
"@eslint/js": "^10.0.1",
"@sveltejs/kit": "^2.57.1",
"@sveltejs/vite-plugin-svelte": "^7.0.0",
"@types/node": "^25.6.0",
"eslint": "^10.2.0",
"@eslint/compat": "^1.2.5",
"@eslint/js": "^9.18.0",
"@sveltejs/adapter-auto": "^6.0.0",
"@sveltejs/kit": "^2.16.0",
"@sveltejs/vite-plugin-svelte": "^5.0.0",
"eslint": "^9.28.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-svelte": "^3.17.0",
"globals": "^17.5.0",
"prettier": "^3.8.3",
"prettier-plugin-svelte": "^3.5.1",
"svelte": "^5.55.4",
"svelte-check": "^4.4.6",
"typescript": "^6.0.2",
"vite": "^8.0.8"
"eslint-plugin-svelte": "^3.9.1",
"globals": "^16.0.0",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"vite": "^6.2.6"
},
"dependencies": {
"@sveltejs/adapter-node": "^5.5.4",
"accept-language-parser": "^1.5.0",
"sveltekit-i18n": "^2.4.2"
}

View file

@ -1,44 +1,92 @@
<!doctype html>
<html lang="en">
<head>
<script
data-name="BMC-Widget"
data-cfasync="false"
src="https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js"
data-id="embroideryviewerxyz"
data-description="Support me on Buy me a coffee!"
data-message="Enjoying Embroidery Viewer?
Buy me a coffee and help keep it running ☕"
data-color="#FFDD03"
data-position="Right"
data-x_margin="18"
data-y_margin="18"
></script>
<!-- Basic -->
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="author" content="Embroidery Viewer" />
<meta name="theme-color" content="#ffffff" />
<style>
:root {
font-family: Inter, Avenir, Helvetica, Arial, sans-serif;
font-size: 16px;
line-height: 24px;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
* {
box-sizing: border-box;
}
body {
display: flex;
justify-content: center;
flex-direction: column;
margin: 0;
width: 100%;
height: 100%;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
background-color: #f2f6f5;
z-index: 10;
}
input[type='submit'] {
width: 100%;
font-size: 20px;
margin-top: 20px;
background-color: #05345f;
font-weight: 700;
color: white;
padding: 10px;
-webkit-appearance: none;
border-radius: 0;
}
input[type='submit']:hover {
cursor: pointer;
background-color: black;
color: white;
}
body a {
text-decoration: none;
color: #06345f;
border-bottom: 3px solid #06345f;
}
body a:hover {
background-color: #06345f;
color: #ffffff;
}
:is(h1, h2, h3, h4, h5, h6) {
color: #06345f;
}
strong {
color: #06345f;
}
ul li::marker {
color: #06345f;
}
</style>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- Favicon -->
<link rel="icon" type="image/png" href="/favicon-96x96.png" sizes="96x96" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="shortcut icon" href="/favicon.ico" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="canonical" href="https://embroideryviewer.xyz/" />
<!-- Mobile / PWA friendliness -->
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link
rel="preload"
href="/fonts/merienda.regular.woff2"
as="font"
type="font/woff2"
crossorigin="anonymous"
/>
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">

543
src/lib/assets/bitcoin.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 95 KiB

1230
src/lib/assets/monero.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 155 KiB

1013
src/lib/assets/paypal.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 130 KiB

View file

@ -1,9 +0,0 @@
<script>
import { isDevelopment } from '$lib/utils/env';
</script>
<svelte:head>
{#if !isDevelopment()}
<script async src="https://hk.leomurca.xyz/hk.js"></script>
{/if}
</svelte:head>

View file

@ -1,69 +0,0 @@
<script>
import { t } from '$lib/translations';
const trackEvent = () => {
// @ts-ignore
window.hk?.event?.('install_now');
};
</script>
<div class="bar">
<div class="content">
<p>
{$t('announcement.message')}
<a
href="https://play.google.com/store/apps/details?id=xyz.embroideryviewer.android"
target="_blank"
rel="noopener noreferrer"
class="cta"
onclick={trackEvent}
>
{$t('announcement.cta-text')}
</a>
</p>
</div>
</div>
<style>
.bar {
width: 100%;
background: #06345f;
color: white;
font-size: 0.95rem;
z-index: 3;
}
.content {
max-width: 1200px;
margin: 0 auto;
padding: 0.6rem 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
}
p {
margin: 0;
line-height: 1.4;
}
.cta {
margin-left: 0.5rem;
font-weight: 600;
text-decoration: underline;
color: #ffffff;
white-space: nowrap;
}
.cta:hover {
opacity: 0.85;
}
@media (max-width: 640px) {
.content {
flex-direction: column;
align-items: flex-start;
}
}
</style>

View file

@ -2,30 +2,17 @@
import { t } from '$lib/translations';
import renderFileToCanvas from '$lib/file-renderer';
/**
* @type {ArrayLike<any>}
*/
export let files = [];
/**
* @type {HTMLElement}
*/
let errorMessageRef;
let canvasRefs = [];
let colorRefs = [];
let stitchesRefs = [];
let sizeRefs = [];
let errorMessageRef;
let localizedStrings = {
stitches: $t('viewer.stitches'),
dimensions: $t('viewer.dimensions'),
};
/**
* Downloads a given HTMLCanvasElement as a PNG image.
*
* @param {HTMLCanvasElement} canvas - The canvas element to export as an image.
* @param {string} filename - The desired name of the downloaded file (extension will be replaced with `.png`).
*/
const downloadCanvasAsImage = (canvas, filename) => {
const image = canvas
.toDataURL('image/png')
@ -37,22 +24,16 @@
link.click();
};
/**
* Cliks the button to render the files when user press enter.
*
* @param {KeyboardEvent} evt - The event that triggered the language switch.
*/
const onKeydown = (evt) => {
if (evt.key === 'Enter') {
const button = document.getElementById('download-button');
if (button) button.click();
document.getElementById('download-button').click();
}
};
</script>
{#if files.length !== 0}
<div id="container" style="width: 100%; heigth: 100vh;">
{#each Array.from(files) as file, i (i)}
{#each Array.from(files) as file, i}
<div class="canvas-container">
<canvas bind:this={canvasRefs[i]} class="canvas"></canvas>
<p><strong>{file.name}</strong></p>
@ -63,8 +44,8 @@
id="download-button"
role="button"
tabindex="0"
onkeydown={onKeydown}
onclick={() => downloadCanvasAsImage(canvasRefs[i], file.name)}
on:keydown={onKeydown}
on:click={() => downloadCanvasAsImage(canvasRefs[i], file.name)}
>
{$t('viewer.download')}
</div>

View file

@ -11,8 +11,6 @@
</script>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
<!-- eslint-disable svelte/no-at-html-tags -->
<div
id="dropzone"
tabindex={0}

View file

@ -1,8 +1,5 @@
<script>
export let title;
/**
* @type {ArrayLike<any>}
*/
export let files = [];
export let isError = false;
</script>
@ -11,7 +8,7 @@
<div id="selected-files-container">
<h2>{title}:</h2>
<div id="files-list">
{#each Array.from(files) as file, i (i)}
{#each Array.from(files) as file}
<div id={isError ? 'selected-file-card-error' : 'selected-file-card'}>
<span>{file.name}</span>
<span>{Math.round(file.size / 1000)} KB</span>

View file

@ -1,185 +1,96 @@
<script>
import { resolve } from '$app/paths';
import { t } from '$lib/translations';
import { appVersion } from '$lib/utils/env';
import MailIcon from '$lib/components/icons/MailIcon.svelte';
import ArrowTopIcon from './icons/ArrowTopIcon.svelte';
import logo from '$lib/assets/logo-white.webp';
</script>
<footer>
<div id="content-container">
<section class="footer-block">
<img
src={logo}
style="height: 80px; width: auto; margin-lft: -5px;"
alt="Logotipo da Embroidery Viewer."
/>
<p>{$t('footer.slogan')}</p>
</section>
<section class="footer-block" aria-labelledby="contact-title">
<h1 id="contact-title">{$t('footer.contact-title')}</h1>
<p>{$t('footer.contact-description')}</p>
<div class="footer-content">
<div class="footer-info">
<p>
{@html $t(
'footer.copyright',
/** @type {any} */ ({
year: new Date().getFullYear(),
website: 'https://leomurca.xyz',
}),
)}
</p>
<p>
{@html $t(
'footer.version',
/** @type {any} */ ({ version: appVersion() }),
)}
</p>
</div>
<address class="contact-container">
<div class="contact-item">
<MailIcon size={30} />
<a href="mailto:leo@leomurca.xyz}" aria-label="leo@leomurca.xyz"
>leo@leomurca.xyz</a
>
</div>
</address>
</section>
<section class="footer-block">
<h1>{$t('footer.resources')}</h1>
<nav class="social-container" aria-label="Social media">
<a href={resolve('/about')}>{$t('footer.about')}</a>
<a href={resolve('/privacy-policy')}>{$t('footer.privacy.policy')}</a>
<a href={resolve('/terms-of-service')}
>{$t('footer.terms.of.service')}</a
>
</nav>
<button
class="back-to-top-button"
aria-label={$t('footer.back-to-top.aria-label')}
onclick={() => scrollTo({ top: 0, behavior: 'smooth' })}
>
<ArrowTopIcon size={30} /> {$t('footer.back-to-top.label')}</button
>
</section>
<nav class="footer-nav">
<a href="/about">{$t('footer.about')}</a>
<a href="/privacy-policy">{$t('footer.privacy.policy')}</a>
<a href="/terms-of-service">{$t('footer.terms.of.service')}</a>
</nav>
</div>
<section class="credits-container">
Copyright {new Date().getFullYear()}
<a href="https://leomurca.xyz" target="_blank" rel="noreferrer"
>Leonardo Murça</a
>
|
{$t('footer.version')}:
<span style="font-family: var(--font-bold);">{appVersion()}</span>
</section>
</footer>
<style>
footer {
background-color: var(--color-primary);
background-color: #f8f9fa;
border-top: 1px solid #ddd;
padding: 20px;
width: 100%;
}
#content-container {
width: 85%;
.footer-content {
display: flex;
justify-content: space-between;
margin: 0 auto;
padding: 60px 0 60px 30px;
scroll-margin-top: 100px;
color: white;
gap: 20px;
}
.footer-block {
width: 100%;
}
h1 {
font-weight: 700;
color: white;
font-size: 1.2rem;
}
.contact-container {
display: flex;
flex-direction: column;
gap: 8px;
letter-spacing: 0.1rem;
font-style: normal;
}
.contact-container a {
color: white;
}
.contact-item {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
}
.contact-item:hover {
font-weight: 700;
}
.social-container {
display: flex;
flex-direction: column;
}
.social-container a {
color: white;
}
.social-container a:hover {
font-weight: 700;
}
.back-to-top-button {
flex-wrap: wrap;
max-width: 960px;
margin: 0 auto;
text-align: center;
background-color: transparent;
border-radius: 20px;
border: none;
padding: 13px;
text-decoration: none;
color: white;
font-size: 1rem;
width: 40%;
margin-top: 30px;
border: 1px solid white;
display: flex;
align-items: center;
gap: 10px;
cursor: pointer;
}
.back-to-top-button:hover {
background-color: #ffffff;
color: var(--color-primary);
.footer-info {
flex: 1 1 100%;
margin-bottom: 10px;
}
.credits-container {
background-color: var(--color-secondary);
color: white;
margin: 0 auto;
padding: 20px 30px;
padding-left: 9%;
width: 100%;
.footer-info p {
margin: 4px 0;
font-size: 14px;
color: #333;
}
.credits-container a {
color: white;
border-bottom: 1px solid white;
.footer-info p:first-child {
font-weight: bold;
}
.credits-container a:hover {
background-color: white;
color: var(--color-secondary);
.footer-nav {
flex: 1 1 100%;
}
/* Responsive */
@media (max-width: 768px) {
#content-container {
width: 100%;
margin: 0;
padding: 60px 30px;
flex-direction: column;
.footer-nav a {
margin: 0 10px;
font-size: 14px;
}
@media (min-width: 600px) {
.footer-content {
flex-wrap: nowrap;
text-align: left;
}
.back-to-top-button {
width: 100%;
justify-content: center;
gap: 5px;
.footer-info,
.footer-nav {
flex: 1 1 50%;
margin-bottom: 0;
}
.footer-info {
text-align: left;
}
.footer-nav {
text-align: right;
}
}
</style>

View file

@ -1,89 +0,0 @@
<script>
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
import { locale, t } from '$lib/translations';
import { normalizeLocaleUnderscore } from '$lib/utils/normalizeLocaleUnderscore';
import { isDevelopment } from '$lib/utils/env';
/**
* =========================
* Props
* =========================
*/
/** @type {string} Page title (translation key) */
export let title;
/** @type {string} Page description (translation key) */
export let description;
/** @type {string} SEO keywords (translation key or raw string) */
export let keywords;
/** @type {string} Canonical URL (absolute) */
export let url;
/** @type {string} Open Graph type (e.g., 'website', 'article') */
export let ogType = 'website';
/** @type {string} Optional override for Open Graph description */
export let ogDescription = description;
/** @type {string} Twitter card type */
export let twitterCard = 'summary_large_image';
/** @type {boolean} Whether the page should be indexed */
export let shouldBeIndexed = !isDevelopment();
/**
* =========================
* Derived / Computed values
* =========================
*/
let image = `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/logo-icon.webp`;
// Translations (avoid repeating $t everywhere)
$: translatedTitle = title ? $t(title) : '';
$: translatedDescription = description ? $t(description) : '';
$: translatedKeywords = keywords ? $t(keywords) : '';
// Fallbacks
$: finalOgDescription = ogDescription
? $t(ogDescription)
: translatedDescription;
// Locale formatting (e.g., en-US -> en_US)
$: ogLocale = normalizeLocaleUnderscore($locale);
// Robots directive
$: robotsContent = shouldBeIndexed ? 'index, follow' : 'noindex, nofollow';
</script>
<svelte:head>
<!-- Primary SEO -->
<title>{translatedTitle}</title>
<meta name="description" content={translatedDescription} />
<meta name="keywords" content={translatedKeywords} />
<!-- Robots -->
<meta name="robots" content={robotsContent} />
<meta name="googlebot" content={robotsContent} />
<!-- Open Graph (Facebook, LinkedIn, etc.) -->
<meta property="og:type" content={ogType} />
<meta property="og:title" content={translatedTitle} />
<meta property="og:description" content={finalOgDescription} />
<meta property="og:image" content={image} />
<meta property="og:url" content={url} />
<meta property="og:locale" content={ogLocale} />
<meta property="og:site_name" content="Embroidery Viewer" />
<!-- Twitter -->
<meta name="twitter:card" content={twitterCard} />
<meta name="twitter:title" content={translatedTitle} />
<meta name="twitter:description" content={finalOgDescription} />
<meta name="twitter:image" content={image} />
<!-- Optional: improves link previews in some platforms -->
<meta property="og:image:alt" content={translatedTitle} />
</svelte:head>

View file

@ -1,64 +1,91 @@
<script>
import { t } from '$lib/translations';
import { resolve } from '$app/paths';
import { page } from '$app/state';
import { t, locale, locales, SUPPORTED_LOCALES } from '$lib/translations';
import logo from '$lib/assets/logo.webp';
import LanguageSwitch from './LanguageSwitch.svelte';
import MediaQuery from './MediaQuery.svelte';
let isOpen = $state(false);
let route = $derived(page.url.pathname);
$effect(() => {
route; // track dependency
isOpen = false;
});
const configsFor = (/** @type {boolean} */ matches) => {
return matches
? { src: logo, width: 150, height: 70 } // mobile
: { src: logo, width: 150, height: 100 }; // desktop
};
const onSwitchToOppositeLang = () => {
$locale =
$locale === SUPPORTED_LOCALES.EN_US
? SUPPORTED_LOCALES.PT_BR
: SUPPORTED_LOCALES.EN_US;
};
let isMenuOpen = false;
</script>
<header>
<div class="logo">
<MediaQuery query="(max-width: 768px)" let:matches>
<a href={resolve('/')}>
{@const configs = configsFor(matches)}
<a href="/">
<img
src={logo}
src={configs.src}
alt="Embroidery viewer logo"
width="150"
height={matches ? 70 : 100}
width={configs.width}
height={configs.height}
/>
</a>
</MediaQuery>
</div>
<nav class:active={route !== '/'} id="menuToggle">
<input type="checkbox" bind:checked={isOpen} />
<span></span>
<span></span>
<span></span>
<div class="nav-container">
<MediaQuery query="(max-width: 768px)" let:matches>
<slot let-matches>
{#if matches}
<button class="hamburger" on:click={() => (isMenuOpen = !isMenuOpen)}>
{#if isMenuOpen}x{:else}{/if}
</button>
{/if}
</slot>
</MediaQuery>
<nav class:is-open={isMenuOpen}>
<ul>
<li>
<a href="/">{$t('header.homeNav')}</a>
</li>
<li>
<a href="/viewer">{$t('header.viewerNav')}</a>
</li>
<li>
<a href="/about">{$t('header.aboutNav')}</a>
</li>
<li>
<a href="/donate">{$t('header.donateNav')}</a>
</li>
</ul>
</nav>
<ul id="menu">
<li><a href={resolve('/about')}>{$t('header.aboutNav')}</a></li>
<li><a href={resolve('/viewer')}>{$t('header.viewerNav')}</a></li>
<li><a href={resolve('/support-us')}>{$t('header.supportUsNav')}</a></li>
<li style="font-size: 22px; padding: 10px 0;"><LanguageSwitch /></li>
</ul>
</nav>
<a
class="common-switch {$locale === SUPPORTED_LOCALES.EN_US
? 'portuguese-switch'
: 'english-switch'}"
href="#"
on:click|preventDefault={onSwitchToOppositeLang}
>
<div style="display: flex; width: fit-content;">
<span style="font-size: 20px;">{$t('header.languageSwitch')}</span>
</div>
</a>
</div>
</header>
<style>
header {
position: absolute;
top: 0;
z-index: 2;
display: flex;
align-items: center;
justify-content: space-between;
padding: 45px 30px 10px 100px;
padding: 10px 100px;
background-color: #f8f9fa;
border-bottom: 1px solid #ddd;
width: 100%;
}
.logo {
z-index: -1;
}
.logo img {
height: auto;
max-height: 60px;
@ -71,166 +98,109 @@
.logo a:hover {
background: transparent;
}
#menuToggle {
position: relative;
.nav-container {
display: flex;
flex-direction: column;
align-items: flex-end;
}
/* Click area */
#menuToggle input {
display: block;
width: 40px;
height: 32px;
position: absolute;
top: -7px;
left: -5px;
cursor: pointer;
opacity: 0;
z-index: 2;
}
/* Bars */
#menuToggle span {
display: block;
width: 30px;
height: 3px;
margin-bottom: 7px;
background: #ffffff;
border-radius: 2px;
transition: all 0.35s ease;
transform-origin: 4px 0px;
}
/* Adjust origins for rotation */
#menuToggle span:nth-child(2) {
transform-origin: 0% 0%;
width: 20px;
}
#menuToggle span:nth-child(4) {
transform-origin: 0% 100%;
width: 20px;
}
/* === ANIMATION === */
/* Top bar → rotates */
#menuToggle input:checked ~ span:nth-child(2) {
transform: rotate(45deg) translate(-1px, -1px);
width: 30px;
}
/* Middle bar → fades out */
#menuToggle input:checked ~ span:nth-child(3) {
opacity: 0;
transform: scale(0.2);
}
/* Bottom bar → rotates opposite */
#menuToggle input:checked ~ span:nth-child(4) {
transform: rotate(-45deg) translate(1px, -1px);
width: 30px;
}
#menu {
display: flex;
justify-content: center;
gap: 20px;
align-items: center;
flex-direction: column;
position: absolute;
top: -80px;
width: 400px;
margin: -100px 0 0 0;
padding: 50px;
padding-top: 125px;
right: -100px;
background: var(--color-primary);
list-style-type: none;
-webkit-font-smoothing: antialiased;
transform-origin: 0% 0%;
transform: translate(100%, 0);
transition: transform 0.5s cubic-bezier(0.77, 0.2, 0.05, 1);
z-index: -1;
margin: 30px;
border-radius: 70% 30% 30% 70% / 60% 40% 0 100%;
box-shadow:
0 2.8px 2.2px rgba(0, 0, 0, 0.02),
0 6.7px 5.3px rgba(0, 0, 0, 0.028),
0 12.5px 10px rgba(0, 0, 0, 0.035),
0 22.3px 17.9px rgba(0, 0, 0, 0.042),
0 41.8px 33.4px rgba(0, 0, 0, 0.05),
0 100px 80px rgba(0, 0, 0, 0.07);
}
#menu li a {
padding: 10px 0;
font-size: 22px;
color: white;
display: block;
nav ul {
list-style: none;
margin: 0;
padding: 0;
display: flex;
gap: 20px;
}
#menu li a:hover {
color: #adadad;
transition: all 0.5s ease;
nav ul li {
display: flex;
font-weight: bold;
}
#menuToggle input:checked ~ ul {
transform: scale(1, 1);
opacity: 1;
.hamburger {
background: none;
border: none;
font-size: 35px;
width: 35px;
padding: 0;
margin: 0;
cursor: pointer;
display: none;
}
.active input:checked ~ span:nth-child(2),
.active input:checked ~ span:nth-child(3),
.active input:checked ~ span:nth-child(4) {
background-color: white !important;
.common-switch {
width: fit-content;
}
.active span {
background-color: var(--color-primary) !important;
.portuguese-switch {
color: #0c8f27;
border-bottom: 3px solid #0c8f27 !important;
fill: #0c8f27 !important;
}
@media (max-width: 1458px) {
#menuToggle span {
background-color: var(--color-primary);
}
/* Top bar → rotates */
#menuToggle input:checked ~ span:nth-child(2) {
background-color: white;
}
/* Middle bar → fades out */
#menuToggle input:checked ~ span:nth-child(3) {
background-color: white;
}
/* Bottom bar → rotates opposite */
#menuToggle input:checked ~ span:nth-child(4) {
background-color: white;
}
.portuguese-switch:hover {
background: #0c8f27;
color: #ffffff;
fill: #ffffff !important;
}
@media (max-width: 1159px) {
header {
padding-top: 70px;
}
.english-switch {
color: #be0a2f;
border-bottom: 3px solid #be0a2f;
width: fit-content;
fill: #be0a2f !important;
}
.english-switch:hover {
background: #be0a2f;
color: #ffffff;
fill: #ffffff !important;
}
@media (max-width: 768px) {
header {
padding: 110px 20px 10px 20px;
padding: 10px 20px;
}
.hamburger {
display: block;
width: 35px;
}
#menu {
width: 100vw;
top: -110px;
margin: 0px 0 0 0;
right: -20px;
border-radius: 0;
border-radius: 0% 0% 80% 80%;
transform: translate(0%, -100%);
nav {
display: none;
flex-direction: column;
gap: 10px;
background-color: #f8f9fa;
position: absolute;
top: 60px;
right: 0;
border: 1px solid #ddd;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
nav.is-open {
display: inline-block;
margin-top: 25px;
width: 100%;
}
nav ul {
flex-direction: column;
gap: 0px;
width: 100%;
}
nav ul li {
text-align: center;
width: 100%;
}
nav ul li a {
display: inline-block;
width: 100%;
padding: 10px;
border-bottom: none;
}
}
</style>

View file

@ -1,145 +0,0 @@
<script>
import { locale, SUPPORTED_LOCALES } from '$lib/translations';
const isEnglish = $derived($locale === SUPPORTED_LOCALES.EN_US);
/**
* Switches the current locale to the opposite language (EN_US <-> PT_BR).
* Prevents the default link behavior (e.g., page jump).
*/
const onSwitchToOppositeLang = () => {
$locale =
$locale === SUPPORTED_LOCALES.EN_US
? SUPPORTED_LOCALES.PT_BR
: SUPPORTED_LOCALES.EN_US;
};
</script>
<center>
<div class="switch">
<input
id="language-toggle"
class="check-toggle check-toggle-round-flat"
type="checkbox"
onclick={onSwitchToOppositeLang}
checked={isEnglish}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
onSwitchToOppositeLang();
}
}}
/>
<label for="language-toggle"></label>
<span class="off">EN</span>
<span class="on">PT</span>
</div>
</center>
<style>
.switch {
position: relative;
display: inline-block;
margin: 0;
font-weight: 700;
z-index: 1;
}
.switch > span {
position: absolute;
top: 7px;
pointer-events: none;
font-weight: bold;
font-size: 12px;
text-transform: uppercase;
text-shadow: 0 1px 0 rgba(0, 0, 0, 0.06);
width: 50%;
text-align: center;
}
input.check-toggle-round-flat:checked ~ .off {
color: white !important;
}
input.check-toggle-round-flat:checked ~ .on {
color: var(--color-primary) !important;
}
.switch > span.on {
left: 0;
padding-left: 2px;
color: white;
}
.switch > span.off {
right: 0;
padding-right: 4px;
color: var(--color-primary) !important;
}
.check-toggle {
position: absolute;
margin-left: -9999px;
visibility: hidden;
}
.check-toggle + label {
display: block;
position: relative;
cursor: pointer;
outline: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
input.check-toggle-round-flat + label {
padding: 2px;
width: 97px;
height: 35px;
background-color: white;
-webkit-border-radius: 60px;
-moz-border-radius: 60px;
-ms-border-radius: 60px;
-o-border-radius: 60px;
border-radius: 60px;
}
input.check-toggle-round-flat + label:before,
input.check-toggle-round-flat + label:after {
display: block;
position: absolute;
content: '';
}
input.check-toggle-round-flat + label:before {
top: 2px;
left: 2px;
bottom: 2px;
right: 2px;
background-color: white;
-moz-border-radius: 60px;
-ms-border-radius: 60px;
-o-border-radius: 60px;
border-radius: 60px;
}
input.check-toggle-round-flat + label:after {
top: 4px;
left: 4px;
bottom: 4px;
width: 48px;
background-color: var(--color-primary);
-webkit-border-radius: 52px;
-moz-border-radius: 52px;
-ms-border-radius: 52px;
-o-border-radius: 52px;
border-radius: 52px;
-webkit-transition: margin 0.2s;
-moz-transition: margin 0.2s;
-o-transition: margin 0.2s;
transition: margin 0.2s;
}
input.check-toggle-round-flat:checked + label:after {
margin-left: 42px;
}
</style>

View file

@ -0,0 +1,61 @@
<script>
import { t } from '$lib/translations';
/** @type {string} Title of the page */
export let title;
/** @type {string} Description of the page */
export let description;
/** @type {string} SEO keywords */
export let keywords;
/** @type {string} Canonical URL of the page */
export let url;
/** @type {string} Main image URL for sharing */
export let image;
/** @type {string} Open Graph type (e.g., 'website', 'article') */
export let ogType = 'website';
/** @type {string} Open Graph title (defaults to title) */
export let ogTitle = title;
/** @type {string} Open Graph description (defaults to description) */
export let ogDescription = description;
/** @type {string} Open Graph image (defaults to image) */
export let ogImage = image;
/** @type {string} Twitter card type (e.g., 'summary_large_image') */
export let twitterCard = 'summary_large_image';
/** @type {string} Twitter title (defaults to title) */
export let twitterTitle = title;
/** @type {string} Twitter description (defaults to description) */
export let twitterDescription = description;
/** @type {string} Twitter image (defaults to image) */
export let twitterImage = image;
</script>
<svelte:head>
<title>{$t(title)}</title>
<meta name="description" content={$t(description)} />
<meta name="keywords" content={$t(keywords)} />
<!-- Open Graph -->
<meta property="og:type" content={ogType} />
<meta property="og:title" content={$t(ogTitle)} />
<meta property="og:description" content={$t(ogDescription)} />
<meta property="og:image" content={ogImage} />
<meta property="og:url" content={url} />
<!-- Twitter -->
<meta name="twitter:card" content={twitterCard} />
<meta name="twitter:title" content={twitterTitle} />
<meta name="twitter:description" content={$t(twitterDescription)} />
<meta name="twitter:image" content={twitterImage} />
</svelte:head>

View file

@ -1,14 +0,0 @@
<script>
export let size = 20;
export let color = 'currentColor';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
height={size}
viewBox="0 -960 960 960"
width={size}
fill={color}
>
<path d="M480-528 296-344l-56-56 240-240 240 240-56 56-184-184Z" />
</svg>

View file

@ -1,21 +0,0 @@
<script>
export let size = 20;
export let color = 'currentColor';
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill={color}
aria-hidden="true"
id="Bolt--Streamline-Heroicons"
height={size}
width={size}
>
<path
fill-rule="evenodd"
d="M9.743333333333332 1.0633333333333332a0.5 0.5 0 0 1 0.23933333333333331 0.568L8.654666666666666 6.5h4.845333333333333a0.5 0.5 0 0 1 0.36533333333333334 0.8413333333333333l-7 7.5a0.5 0.5 0 0 1 -0.848 -0.4733333333333333l1.3279999999999998 -4.867999999999999H2.5a0.5 0.5 0 0 1 -0.36533333333333334 -0.8413333333333333l7 -7.5a0.5 0.5 0 0 1 0.6086666666666667 -0.09533333333333333Z"
clip-rule="evenodd"
stroke-width="0.6667"
></path>
</svg>

File diff suppressed because one or more lines are too long

View file

@ -1,21 +0,0 @@
<script>
export let size = 20;
export let color = 'currentColor';
</script>
<svg
width={size}
height={size}
viewBox="0 0 115.03736 82.932701"
version="1.1"
id="svg1"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
><defs id="defs1" /><g id="layer1" transform="translate(46.342105,-326.35286)"
><path
style={`fill:${color}`}
d="m -33.360673,409.11068 c -0.52052,-0.66147 -13.05506,-29.41883 -12.98111,-29.7819 0.0676,-0.33192 10.25008,-3.75875 11.16875,-3.75875 0.39694,0 -0.23972,-1.36265 6.66385,14.2629 3.11223,7.04422 5.6586,13.00337 5.6586,13.24254 0,0.28356 -1.49602,1.30029 -4.29948,2.92205 -2.36471,1.36794 -4.67009,2.70349 -5.12306,2.96789 -0.65389,0.38166 -0.87798,0.4116 -1.08755,0.14527 z m 51.11115,-7.18941 c -1.0255,-0.19444 -6.26425,-1.60693 -11.6416602,-3.13887 -11.60606,-3.30637 -11.88394,-3.36862 -15.03947,-3.36862 -3.0879598,0 -5.4300198,0.58865 -9.3851798,2.35884 -1.65323,0.73993 -3.20801,1.34533 -3.45507,1.34533 -0.33715,0 -1.59126,-2.59045 -5.02761,-10.38489 -2.51812,-5.7117 -4.63162,-10.51906 -4.69666,-10.68304 -0.065,-0.16398 0.75205,-0.77969 1.81577,-1.36826 1.06372,-0.58856 3.06512,-1.98586 4.44757,-3.1051 1.38245,-1.11925 3.21341,-2.51262 4.06879,-3.09639 5.178,-3.5338 12.3545098,-5.07308 18.9419698,-4.06285 3.59287,0.55099 6.45084,1.42963 10.85591,3.33749 7.3390302,3.17857 11.7920602,4.46814 22.2961402,6.45677 6.83656,1.29429 8.39146,1.84388 9.38771,3.31816 1.351,1.99922 0.46493,4.08861 -2.34074,5.5196 -2.74024,1.39761 -3.54371,1.37687 -13.73269,-0.35444 -8.2986,-1.4101 -9.07435,-1.4986 -12.17084,-1.38851 -2.5834002,0.0919 -3.7126702,0.25703 -5.1593702,0.75463 -1.63768,0.56329 -1.86179,0.72144 -1.93595,1.36615 -0.12182,1.05906 0.46688,1.15204 2.81088,0.44394 3.5053302,-1.05892 6.4809702,-0.89831 16.9844402,0.91671 8.37829,1.44778 8.95895,1.5149 10.60612,1.22601 4.34498,-0.76205 7.62602,-3.6216 7.64432,-6.66234 0.003,-0.56374 -0.31433,-1.57393 -0.76375,-2.42828 -0.88082,-1.67447 -0.97521,-1.4883 1.59586,-3.14763 1.45305,-0.93779 3.67139,-2.74223 8.64098,-7.02876 5.07066,-4.37371 7.8612,-5.88252 9.87621,-5.33994 0.5539,0.14915 1.07867,0.38701 1.16616,0.52858 0.0875,0.14156 0.75876,0.25739 1.49171,0.25739 2.23308,0 3.66323,1.33775 3.66323,3.42656 0,1.22305 -0.39272,1.86203 -5.78067,9.40573 -5.63224,7.88573 -7.41725,9.89443 -10.15569,11.42836 -5.52566,3.09519 -23.79832,12.65577 -25.06445,13.11418 -1.88226,0.68147 -7.21439,0.87102 -9.94397,0.35349 z m 8.93119,-37.4527 c -10.54204,-7.34003 -15.45466,-12.20175 -18.3280302,-18.13812 -2.47555,-5.11448 -2.73882,-10.04227 -0.74529,-13.94992 0.92633,-1.81576 3.3031302,-4.09338 5.2439402,-5.02511 3.14838,-1.51146 7.45008,-1.29668 10.56261,0.52739 1.77607,1.04084 4.13023,3.35279 5.47731,5.37909 0.66799,1.00478 1.26128,1.82688 1.31843,1.82688 0.0571,0 0.69136,-0.8632 1.40937,-1.91822 2.27222,-3.33879 4.61009,-5.23911 7.75182,-6.30101 2.33309,-0.78858 5.77205,-0.63428 7.95915,0.35711 1.75477,0.79542 4.18097,2.97573 5.14853,4.62674 4.08542,6.97124 0.45301,16.98315 -9.18256,25.30961 -4.39191,3.79521 -12.25534,9.42128 -13.13598,9.39843 -0.27047,-0.007 -1.83615,-0.94881 -3.4793,-2.09287 z"
id="path2"
/></g
></svg
>

View file

@ -1,17 +0,0 @@
<script>
export let size = 20;
export let color = 'currentColor';
</script>
<svg
fill={color}
width={size}
height={size}
viewBox="0 0 256 256"
id="Flat"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M213.65723,66.34326l-40-40A8.00076,8.00076,0,0,0,168,24H88A16.01833,16.01833,0,0,0,72,40V56H56A16.01833,16.01833,0,0,0,40,72V216a16.01833,16.01833,0,0,0,16,16H168a16.01833,16.01833,0,0,0,16-16V200h16a16.01833,16.01833,0,0,0,16-16V72A8.00035,8.00035,0,0,0,213.65723,66.34326ZM136,192H88a8,8,0,0,1,0-16h48a8,8,0,0,1,0,16Zm0-32H88a8,8,0,0,1,0-16h48a8,8,0,0,1,0,16Zm64,24H184V104a8.00035,8.00035,0,0,0-2.34277-5.65674l-40-40A8.00076,8.00076,0,0,0,136,56H88V40h76.68652L200,75.314Z"
/>
</svg>

View file

@ -1,18 +0,0 @@
<script>
export let size = 20;
export let color = 'currentColor';
</script>
<svg
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke={color}
stroke-width="1.5"
>
<rect x="3" y="3" width="7" height="7" rx="1.5" />
<rect x="14" y="3" width="7" height="7" rx="1.5" />
<rect x="3" y="14" width="7" height="7" rx="1.5" />
<rect x="14" y="14" width="7" height="7" rx="1.5" />
</svg>

View file

@ -1,19 +0,0 @@
<script>
export let size = 20;
export let strokeWidth = 2;
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width={strokeWidth}
stroke-linecap="round"
stroke-linejoin="round"
>
<rect x="3" y="5" width="18" height="14" rx="2" ry="2" />
<polyline points="3,7 12,13 21,7" />
</svg>

View file

@ -1,17 +0,0 @@
<script>
export let size = 20;
export let color = 'currentColor';
</script>
<svg
fill={color}
width={size}
height={size}
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
id="lock-check"
class="icon glyph"
><path
d="M18,8H17V7A5,5,0,0,0,7,7V8H6a2,2,0,0,0-2,2V20a2,2,0,0,0,2,2H18a2,2,0,0,0,2-2V10A2,2,0,0,0,18,8ZM9,7a3,3,0,0,1,6,0V8H9Zm6.71,6.71-4,4a1,1,0,0,1-1.42,0l-2-2a1,1,0,0,1,1.42-1.42L11,15.59l3.29-3.3a1,1,0,0,1,1.42,1.42Z"
></path></svg
>

View file

@ -1,93 +0,0 @@
<script>
import { t } from '$lib/translations';
</script>
<section id="faq">
<h1>{$t('faq.title')}</h1>
<p class="intro">
{$t('faq.intro')}
</p>
<div class="faq-list">
<details>
<summary>{$t('faq.items.openPesOnline.summary')}</summary>
<p>{$t('faq.items.openPesOnline.description')}</p>
</details>
<details>
<summary>{$t('faq.items.supportedFormats.summary')}</summary>
<p>{$t('faq.items.supportedFormats.description')}</p>
</details>
<details>
<summary>{$t('faq.items.needSoftware.summary')}</summary>
<p>{$t('faq.items.needSoftware.description')}</p>
</details>
<details>
<summary>{$t('faq.items.isSafe.summary')}</summary>
<p>{$t('faq.items.isSafe.description')}</p>
</details>
<details>
<summary>{$t('faq.items.multipleFiles.summary')}</summary>
<p>{$t('faq.items.multipleFiles.description')}</p>
</details>
<details>
<summary>{$t('faq.items.mobileSupport.summary')}</summary>
<p>{$t('faq.items.mobileSupport.description')}</p>
</details>
</div>
</section>
<style>
#faq {
padding: 100px 20px;
max-width: 800px;
margin: 0 auto;
}
#faq h1 {
font-weight: 700;
font-size: 2.5rem;
text-align: center;
line-height: 1.5;
}
#faq .intro {
text-align: center;
margin: 10px 0 40px;
}
.faq-list details {
border-bottom: 1px solid #eee;
padding: 20px 0;
}
.faq-list summary {
cursor: pointer;
font-weight: 600;
font-size: 1.2rem;
list-style: none;
position: relative;
color: var(--color-primary);
}
.faq-list summary::after {
content: '+';
position: absolute;
right: 0;
font-size: 1.7rem;
transition: transform 0.3s ease;
}
.faq-list details[open] summary::after {
transform: rotate(45deg);
}
.faq-list p {
margin-top: 10px;
line-height: 1.6;
}
</style>

View file

@ -1,224 +0,0 @@
<script>
import { resolve } from '$app/paths';
import { t } from '$lib/translations';
import BoltIcon from '$lib/components/icons/BoltIcon.svelte';
import PadlockIcon from '$lib/components/icons/PadlockIcon.svelte';
import FourSquaresIcon from '$lib/components/icons/FourSquaresIcon.svelte';
import FilesIcon from '$lib/components/icons/FilesIcon.svelte';
</script>
<section id="features">
<header>
<h1>{$t('features.title')}</h1>
<p class="subtitle">{$t('features.subtitle')}</p>
</header>
<div class="cards-container">
<div class="blob">
<p class="adjective">{$t('features.cards.fast.adjective')}</p>
<h2>{$t('features.cards.fast.title')}</h2>
<BoltIcon color="#06345f" size={100} />
<p class="description">{$t('features.cards.fast.description')}</p>
</div>
<div class="blob">
<p class="adjective">{$t('features.cards.private.adjective')}</p>
<h2>{$t('features.cards.private.title')}</h2>
<PadlockIcon color="#06345f" size={100} />
<p class="description">{$t('features.cards.private.description')}</p>
</div>
<div class="blob">
<p class="adjective">{$t('features.cards.optimized.adjective')}</p>
<h2>{$t('features.cards.optimized.title')}</h2>
<FourSquaresIcon color="#06345f" size={100} />
<p class="description">{$t('features.cards.optimized.description')}</p>
</div>
<div class="blob">
<p class="adjective">{$t('features.cards.compatibility.adjective')}</p>
<h2>{$t('features.cards.compatibility.title')}</h2>
<FilesIcon color="#06345f" size={100} />
<p class="description">
{$t('features.cards.compatibility.description')}
</p>
</div>
</div>
<a class="organic-btn-secondary" href={resolve('/viewer')}
>{$t('features.cta')}</a
>
</section>
<style>
#features {
display: flex;
align-items: center;
flex-direction: column;
padding: 100px 0;
background-color: var(--color-primary);
position: relative;
overflow: hidden;
}
#features::before {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg width='600' height='600' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M10 150 C 60 20, 140 180, 190 50' stroke='white' stroke-width='1.5' stroke-dasharray='4 6' opacity='0.2'/%3E%3Ccircle cx='10' cy='150' r='3' fill='white' opacity='0.25'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-size: 600px;
background-position: right -100px top -80px;
pointer-events: none;
}
#features::after {
content: '';
position: absolute;
inset: 0;
background-image: url("data:image/svg+xml,%3Csvg width='600' height='600' viewBox='0 0 200 200' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M20 40 C 80 180, 120 0, 180 140' stroke='white' stroke-width='1.2' stroke-dasharray='3 8' opacity='0.15'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-size: 500px;
background-position: left -120px bottom -80px;
pointer-events: none;
}
header {
text-align: center;
line-height: 1.5;
width: 80%;
}
h1 {
color: white;
font-size: 2.7rem;
}
.subtitle {
color: white;
font-size: 1.2rem;
text-align: center;
}
.cards-container {
display: grid;
gap: 20px;
grid-template-columns: repeat(4, 1fr);
margin: 0 auto;
width: 100%;
padding: 100px 40px;
}
.blob {
width: 100%;
max-width: 380px;
height: 400px;
aspect-ratio: 1;
border-radius: 25% 50% 75% 100%;
text-align: center;
padding: 30px 60px;
background: linear-gradient(
30deg in oklch shorter hue,
oklch(0.98 0.01 260) 10%,
oklch(0.92 0.02 260)
);
box-shadow: 1rem 1rem 50px #0001;
}
.blob h2 {
font-size: 1.5rem;
margin-top: 10px;
}
.description {
font-size: 1rem;
margin-top: 20px;
}
.adjective {
color: var(--color-primary);
font-size: 0.8rem !important;
}
.organic-btn-secondary {
font-size: 1.3rem;
padding: 30px 90px;
}
@media (max-width: 1639px) {
.blob {
width: 100%;
max-width: 340px;
height: 380px;
}
}
@media (max-width: 1480px) {
.cards-container {
grid-template-columns: repeat(3, 1fr);
}
.blob {
width: 100%;
max-width: 400px;
height: 400px;
}
}
@media (max-width: 1297px) {
.cards-container {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 1210px) {
h1 {
font-size: 2.3rem;
}
.subtitle {
font-size: 1rem;
}
}
@media (max-width: 1033px) {
h1 {
font-size: 2rem;
}
}
@media (max-width: 880px) {
#features {
padding: 20px;
}
header {
width: 100%;
}
h1 {
font-size: 2rem;
}
.cards-container {
padding: 0;
grid-template-columns: 1fr;
}
.blob {
width: 100%;
max-width: 100%;
padding-bottom: 50px;
height: fit-content;
aspect-ratio: 0;
}
}
@media (max-width: 540px) {
.organic-btn-secondary {
width: 100%;
text-align: center;
font-size: 1rem;
padding: 20px 50px;
margin-top: 30px;
}
}
</style>

View file

@ -1,97 +0,0 @@
<script>
import { resolve } from '$app/paths';
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
import { t } from '$lib/translations';
import { isMobile } from '$lib/utils/isMobile';
const backgroundImage = isMobile()
? `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/hero-mobile.webp`
: `${PUBLIC_IMAGE_BASE_URL}/t/f_webp,w_1920,h_1080/embroidery-viewer/hero.webp`;
</script>
<section
id="hero"
style={`background: url(${backgroundImage}) center/cover no-repeat`}
>
<div class="overlay">
<h1>{$t('hero.title')}</h1>
<p>{$t('hero.description')}</p>
<a class="organic-btn" href={resolve('/viewer')}>{$t('hero.cta')}</a>
</div>
</section>
<style>
#hero {
position: relative;
z-index: 0;
min-height: 100vh;
display: flex;
}
.overlay {
display: flex;
flex-direction: column;
max-width: 800px;
z-index: 1;
padding-left: 100px;
padding-top: 130px;
}
h1 {
font-size: clamp(3.5rem, 4vw, 3.5rem);
font-weight: 700;
margin-bottom: 1rem;
line-height: 1.2;
}
p {
font-size: 1.5rem;
margin-bottom: 2rem;
}
@media (max-width: 1350px) {
h1 {
font-size: clamp(3.3rem, 4vw, 3.3rem);
}
}
@media (max-width: 1280px) {
h1 {
font-size: clamp(3rem, 4vw, 3rem);
}
p {
font-size: 1.1rem;
}
}
@media (max-width: 1180px) {
h1 {
font-size: clamp(3rem, 4vw, 3rem);
}
}
@media (max-width: 768px) {
h1 {
font-size: clamp(2rem, 4vw, 2rem);
}
p {
margin-top: 0;
}
#hero {
background-size: contain !important;
background-position: bottom !important;
}
.overlay {
width: 100%;
padding-top: 100px;
padding-left: 0;
height: 100vh;
padding-left: 20px;
padding-right: 20px;
}
}
</style>

View file

@ -1,194 +0,0 @@
<script>
import { locale, t } from '$lib/translations';
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
</script>
<section class="app">
<div class="container">
<!-- LEFT: TEXT -->
<div class="content">
<h2>
{$t('mobile.title.prefix')}
<span>{$t('mobile.title.highlight')}</span>
</h2>
<p class="description">
{$t('mobile.description')}
</p>
<!-- FEATURES -->
<div class="features">
<div>{$t('mobile.features.formats')}</div>
<div>🎨 {$t('mobile.features.customization')}</div>
<div>🎯 {$t('mobile.features.highlight')}</div>
<div>📏 {$t('mobile.features.metadata')}</div>
<div>🚀 {$t('mobile.features.performance')}</div>
<div>{$t('mobile.features.accessibility')}</div>
</div>
<!-- CTA -->
<a
class="cta-link"
href="https://play.google.com/store/apps/details?id=xyz.embroideryviewer.android"
target="_blank"
>
<img
src={`${PUBLIC_IMAGE_BASE_URL}/t/w_240,h_76,f_webp/embroidery-viewer/${$locale}/android-download.webp`}
alt={$t('mobile.cta_alt')}
width="240"
height="76"
/>
</a>
</div>
<!-- RIGHT: IMAGE -->
<div class="visual">
<img
src={`${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/${$locale}/app-with-frame.webp`}
alt={$t('mobile.image_alt')}
/>
</div>
</div>
<div class="blob"></div>
</section>
<style>
.cta-link {
padding: 0;
margin-top: 50px;
width: fit-content;
height: fit-content;
border: none;
}
.cta-link:hover {
border: none;
background-color: transparent;
}
.app {
position: relative;
padding: 6rem 1.5rem;
overflow: hidden;
/* Base */
background-color: #ffffff;
/* Embroidery-inspired layers */
background-image:
/* soft radial "fabric tension" */
radial-gradient(
circle at 20% 30%,
rgba(6, 52, 95, 0.06),
transparent 60%
),
radial-gradient(
circle at 80% 70%,
rgba(6, 52, 95, 0.05),
transparent 60%
),
/* diagonal "thread lines" */
repeating-linear-gradient(
45deg,
rgba(6, 52, 95, 0.04) 0px,
rgba(6, 52, 95, 0.04) 1px,
transparent 1px,
transparent 12px
),
/* opposite direction stitching */
repeating-linear-gradient(
-45deg,
rgba(6, 52, 95, 0.025) 0px,
rgba(6, 52, 95, 0.025) 1px,
transparent 1px,
transparent 14px
);
}
.app::before {
content: '';
position: absolute;
inset: 0;
pointer-events: none;
background-image:
linear-gradient(rgba(6, 52, 95, 0.03) 1px, transparent 1px),
linear-gradient(90deg, rgba(6, 52, 95, 0.03) 1px, transparent 1px);
background-size: 40px 40px;
}
.blob {
position: absolute;
width: 500px;
height: 500px;
background: radial-gradient(circle, rgba(6, 52, 95, 0.15), transparent 70%);
filter: blur(80px);
top: -100px;
right: -100px;
z-index: 0;
}
.container {
max-width: 1100px;
margin: 0 auto;
display: grid;
grid-template-columns: 1.1fr 1fr;
gap: 3rem;
align-items: center;
}
.content {
display: flex;
flex-direction: column;
gap: 1.2rem;
}
h2 {
font-size: clamp(2rem, 4vw, 2.6rem);
color: #06345f;
margin: 0;
line-height: 1.2;
}
h2 span {
font-weight: 900;
background: linear-gradient(120deg, #06345f, #194795);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.description {
font-size: 1.1rem;
}
.features {
margin-top: 0.5rem;
display: grid;
gap: 0.4rem;
font-size: 0.95rem;
color: #06345f;
}
.visual {
display: flex;
justify-content: center;
}
.visual img {
width: 100%;
max-width: 380px;
}
@media (max-width: 900px) {
.container {
grid-template-columns: 1fr;
text-align: center;
}
.content {
align-items: center;
}
}
</style>

View file

@ -1,59 +0,0 @@
@font-face {
font-family: 'Merienda';
font-display: swap;
src:
url('/fonts/merienda.regular.woff2') format('woff2'),
url('/fonts/merienda.regular.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
font-optical-sizing: auto;
}
@font-face {
font-family: 'Merienda';
font-display: swap;
src:
url('/fonts/merienda.medium.woff2') format('woff2'),
url('/fonts/merienda.medium.woff') format('woff');
font-weight: 500;
font-style: normal;
font-display: swap;
font-optical-sizing: auto;
}
@font-face {
font-family: 'Merienda';
font-display: swap;
src:
url('/fonts/merienda.semi-bold.woff2') format('woff2'),
url('/fonts/merienda.semi-bold.woff') format('woff');
font-weight: 600;
font-style: normal;
font-display: swap;
font-optical-sizing: auto;
}
@font-face {
font-family: 'Merienda';
font-display: swap;
src:
url('/fonts/merienda.bold.woff2') format('woff2'),
url('/fonts/merienda.bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
font-optical-sizing: auto;
}
@font-face {
font-family: 'Merienda';
font-display: swap;
src:
url('/fonts/merienda.extra-bold.woff2') format('woff2'),
url('/fonts/merienda.extra-bold.woff') format('woff');
font-weight: 900;
font-style: normal;
font-display: swap;
font-optical-sizing: auto;
}

View file

@ -1,117 +0,0 @@
:root {
font-family: 'Merienda', cursive;
font-size: 16px;
line-height: 24px;
font-weight: 400;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
* {
box-sizing: border-box;
}
html,
body {
overflow-x: hidden;
}
body {
display: flex;
position: relative;
justify-content: center;
flex-direction: column;
margin: 0;
width: 100%;
height: 100%;
}
#app {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
background-color: #f2f6f5;
z-index: 10;
}
input[type='submit'] {
width: 100%;
font-size: 20px;
margin-top: 20px;
background-color: #05345f;
font-weight: 700;
color: white;
padding: 10px;
-webkit-appearance: none;
border-radius: 0;
}
input[type='submit']:hover {
cursor: pointer;
background-color: black;
color: white;
}
body a {
text-decoration: none;
color: #06345f;
border-bottom: 3px solid #06345f;
}
body a:hover {
background-color: #06345f;
color: #ffffff;
}
:is(h1, h2, h3, h4, h5, h6) {
color: #06345f;
}
strong {
color: #06345f;
}
ul li::marker {
color: #06345f;
}
.organic-btn {
background: var(--color-primary);
width: fit-content;
color: white;
border: none;
padding: 20px 60px;
font-size: 16px;
cursor: pointer;
border-radius: 58% 42% 65% 27% / 40% 60% 60% 70%;
border: 1px solid var(--color-primary);
transition: all 0.3s ease;
}
.organic-btn-secondary {
color: white;
width: fit-content;
padding: 20px 60px;
font-size: 16px;
cursor: pointer;
border-radius: 58% 42% 65% 27% / 40% 60% 60% 70%;
border: 1px solid white;
transition: all 0.3s ease;
background-color: var(--color-primary);
}
.organic-btn:hover {
color: var(--color-primary);
background-color: #ffffff;
border: 1px solid var(--color-primary);
}
.organic-btn-secondary:hover {
color: var(--color-primary);
background-color: #ffffff;
border: 1px solid white;
}

View file

@ -1,6 +0,0 @@
:root {
--color-primary: #06345f;
--color-secondary: #094275;
--font-base: 'Merienda';
}

View file

@ -1,34 +1,9 @@
{
"seo.title": "About Embroidery Viewer The Story Behind the Tool",
"title": " About Embroidery Viewer",
"content": "<p>Hi there! 👋</p><p><strong>⭐️ Embroidery Viewer</strong> was born out of a simple need — helping someone I care about. 💖</p><p>My girlfriend loves embroidery, but she often struggled to find an easy and free way to preview her embroidery design files before stitching them. Most tools she tried were either paid, overly complex, or required technical knowledge — and shes not a techie.</p><p>So, to make things easier for her (and others like her), I decided to build this web application.</p><p>Over the course of a few weeks, I created <strong>Embroidery Viewer</strong> — a lightweight, fast, and free tool that lets you view embroidery files directly in your browser. No installation, no setup, and no tech hurdles. Just upload your file and see your design.</p><p>Its not a super sophisticated tool, but it solves the problem it was meant to solve: making embroidery file previews accessible to everyone.</p><p>If this tool has helped you too, that makes me really happy! I plan to continue improving it based on feedback from users like you.</p><p>Thanks for stopping by — and happy stitching! 🧵✨</p>",
"seo.title": " About Embroidery Viewer The Story Behind the Tool",
"seo.description": "Learn the story behind Embroidery Viewer — a free, online tool created to make embroidery file previews simple, fast, and accessible to everyone.",
"seo.keywords": "about embroidery viewer, embroidery viewer story, free embroidery viewer, why embroidery viewer was created, who created embroidery viewer, online embroidery viewer, free embroidery tool, embroidery viewer about",
"seo.url": "https://embroideryviewer.xyz/about",
"seo.image": "https://embroideryviewer.xyz/og/about.png",
"hero": {
"tagline": "A SMALL ACT OF LOVE",
"title": "That became a powerful way to preview embroidery files"
},
"story": {
"title": "The story behind Embroidery Viewer...",
"p1": "It didnt start as a product. It started with a simple frustration.",
"p2": "Someone I care about loves embroidery. She enjoys creating, exploring designs, and bringing ideas to life through stitching. But every time she downloaded a new embroidery file, she ran into the same problem:",
"quote1": "“How do I even open this?”",
"p3": "Most of the tools available were either paid, overly complex, or built for people already familiar with technical embroidery software. For someone who just wanted to quickly preview a design before stitching, it felt unnecessarily difficult.",
"p4": "What should have been a simple step—just seeing the design—became a barrier. Thats when I realized something important:",
"quote2": "There are probably many people facing the exact same problem."
},
"product": {
"p1": "People dont want complicated software.",
"p2": "People dont want to install anything.",
"p3": "People just want to open an embroidery file and see their design instantly.",
"p4": "So instead of searching for a better tool…",
"quote": "I decided to build one."
},
"support": {
"title": "If it helped you, you can help it grow",
"p1": "Embroidery Viewer is completely free—and I intend to keep it that way.",
"p2": "But keeping it fast, improving support for more embroidery formats, and maintaining the infrastructure behind it takes time and resources.",
"p3": "If this tool made your workflow easier, helped you preview a design, or saved you from unnecessary hassle, you can support its future.",
"cta": "Support Us"
}
"seo.image": "https://embroideryviewer.xyz/og/about.png"
}

View file

@ -1,4 +0,0 @@
{
"message": "🚀 Embroidery Viewer is now on Android — view your designs anywhere, customize thread & background colors.",
"cta-text": "Install now"
}

View file

@ -0,0 +1,18 @@
{
"title": "💖 Donate",
"subtitle": "Help support Embroidery Viewer and its development!",
"description": "⭐️ <strong>Embroidery Viewer</strong> is free to use. If you find this tool helpful, please consider making a donation to keep it running and fund future improvements.",
"ways": "💸 Ways to Donate",
"bitcoin.description": "Scan or copy the address",
"copy": "Copy Address",
"copied": "Copied to Clipboard!",
"copy.failed": "Copy Failed!",
"monero.description": "Private and secure donation option.",
"paypal.description": "Want to show support in a friendly way?",
"paypal.link": "Open Donation link",
"seo.title": "💖 Donate Support Embroidery Viewer",
"seo.description": "Help keep Embroidery Viewer free and improving by making a donation. Choose from Bitcoin, Monero, PayPal, or other secure options to support ongoing development and hosting.",
"keywords": "donate embroidery viewer, support embroidery viewer, embroidery viewer donations, help embroidery viewer, fund embroidery viewer, bitcoin donation embroidery, monero donation embroidery, paypal donation embroidery",
"url": "https://embroideryviewer.xyz/donate",
"image": "https://embroideryviewer.xyz/og/donate.png"
}

View file

@ -1,30 +0,0 @@
{
"title": "Frequently Asked Questions",
"intro": "Learn how to open embroidery files online, supported formats, and how Embroidery Viewer works — all in one place.",
"items": {
"openPesOnline": {
"summary": "How can I open a PES file online?",
"description": "You can open a PES file instantly using Embroidery Viewer. Just drag and drop your file into the browser to preview your embroidery design—no software installation required."
},
"supportedFormats": {
"summary": "What embroidery file formats are supported?",
"description": "Embroidery Viewer supports popular formats such as PES, DST, and EXP. This allows you to preview most embroidery designs used by home and commercial machines."
},
"needSoftware": {
"summary": "Do I need to install any embroidery software?",
"description": "No. Embroidery Viewer works entirely in your browser, so you can open embroidery files online without downloading or installing any software."
},
"isSafe": {
"summary": "Is it safe to use Embroidery Viewer?",
"description": "Yes. Your files are processed locally in your browser and are never uploaded to any server, ensuring complete privacy."
},
"multipleFiles": {
"summary": "Can I view multiple embroidery files at once?",
"description": "Yes. You can open and compare multiple embroidery files in a single view, making it easier to review and choose designs."
},
"mobileSupport": {
"summary": "Can I use this on mobile or tablet?",
"description": "Yes. Embroidery Viewer works on desktop, tablet, and mobile devices, as long as you are using a modern web browser."
}
}
}

View file

@ -1,27 +0,0 @@
{
"title": "The Easiest Way to Preview Embroidery Files",
"subtitle": "Whether you're a hobbyist working on your next DIY project or a professional digitizer reviewing client files, Embroidery Viewer gives you a fast, simple way to preview embroidery designs online—no software, no friction.",
"cards": {
"fast": {
"adjective": "FAST",
"title": "Instant Preview",
"description": "Drop your file and see your design instantly—no installs, no waiting."
},
"private": {
"adjective": "PRIVATE",
"title": "Private by Design",
"description": "Your files stay on your device. Nothing is uploaded, ever."
},
"optimized": {
"adjective": "OPTIMIZED",
"title": "Multiple Files, One View",
"description": "Open and compare several designs at the same time in a clean layout."
},
"compatibility": {
"adjective": "COMPATIBILITY",
"title": "Supports Popular Formats",
"description": "Works with PES, DST, EXP and other common embroidery formats."
}
},
"cta": "Try it now"
}

View file

@ -1,15 +1,7 @@
{
"slogan": "Preview your embroidery designs instantly — no software needed",
"resources": "Resources",
"contact-title": "Contact",
"contact-description": "Do you want to help or have any questions? Contact us.",
"back-to-top": {
"label": "Back To Top",
"aria-label": "Back to top of the page"
},
"about": "About",
"privacy.policy": "Privacy Policy",
"terms.of.service": "Terms of Service",
"copyright": "Copyright © {{year}} <a href=\"{{website}}\" target=\"_blank\" rel=\"noreferrer\">Leonardo Murça</a>. All rights reserved.",
"version": "Version {{version}}"
"about": " About",
"privacy.policy": "🔐 Privacy Policy",
"terms.of.service": "📝 Terms of Service",
"copyright": "Copyright © {{year}} <a href=\"{{website}}\" target=\"_blank\" rel=\"noreferrer\">Leonardo Murça</a>. <br/> All rights reserved.",
"version": "🧵 Version: {{version}}"
}

View file

@ -1,6 +1,7 @@
{
"homeNav": "Home",
"aboutNav": "About",
"viewerNav": "Viewer",
"supportUsNav": "Support Us"
"languageSwitch": "🇧🇷",
"homeNav": "🏠 Home",
"aboutNav": " About",
"viewerNav": "🧵 Viewer",
"donateNav": "💖 Donate"
}

View file

@ -1,5 +0,0 @@
{
"title": "Preview your embroidery designs instantly — no software needed",
"description": "Fast, private & no signup required",
"cta": "Try Your Design"
}

View file

@ -1,6 +1,22 @@
{
"seo.title": "Free Online Embroidery File Viewer - Fast, Private & No Signup",
"main.title": "🧵 Free Online Embroidery File Viewer",
"main.description": "<p>✨Upload and preview your embroidery designs instantly no software needed.</p> <p><strong>Embroidery Viewer</strong> is a free, browser-based tool that supports multiple embroidery file formats. View your designs quickly and securely, right in your browser.</p>",
"features.title": "🚀 Features",
"features.list": "<ul><li>📂 <strong>Supports Multiple Formats:</strong> DST, PES, JEF, EXP, VP3, and more</li><li>⚡ <strong>Quick Previews:</strong> See your embroidery files rendered as images</li><li>🧷 <strong>Multiple Files at Once:</strong> Upload several designs and view them side-by-side</li><li>🔒 <strong>No Upload to Server:</strong> Your files stay private all processing happens locally</li><li>⬇️ <strong>Download as Image:</strong> Save each embroidery design preview as a PNG</li><li>💸 <strong>Fast & Free:</strong> No installations, no sign-ups just open and use</li></ul>",
"howtouse.title": "📘 How to Use",
"howtouse.list": "<ol><li>📁 <strong>Click</strong> the upload button <em>or</em> <strong>drag and drop</strong> your embroidery files into the drop area</li><li>🧵 Select one or more embroidery files</li><li>▶️ Click the <strong>“Render files”</strong> button to preview your designs</li><li>👀 Instantly view your designs right in your browser its that simple</li></ol>",
"testimonials.title": "❤️ Loved by Hobbyists and Professionals",
"testimonials.description": "<p>Whether you're a hobbyist working on your next DIY project or a professional digitizer reviewing client files, <strong>Embroidery Viewer</strong> gives you a no-fuss, instant way to visualize your work.</p>",
"donation.title": "💖 Help Keep It Free",
"donation.description": "<p><strong>Embroidery Viewer is completely free</strong> for everyone to use.</p><p>If you find it useful and want to support ongoing development and hosting costs, please consider making a small donation.</p>",
"donation.cta": "🙌 Donate Now",
"donation.cta.description": "every little bit helps!",
"cta.title": "🚀 Try It Now",
"cta.cta": "🧵 Open Viewer",
"cta.cta.description": "the fastest <strong>Free Online Embroidery File Viewer</strong>.",
"seo.title": "🏠 Free Online Embroidery File Viewer - Fast, Private & No Signup",
"seo.description": "Upload and preview embroidery files instantly with Embroidery Viewer. Supports DST, PES, JEF, EXP, VP3 and more. No installs, no uploads 100% browser-based and free.",
"seo.keywords": "embroidery viewer, online embroidery viewer, embroidery file preview, DST viewer, PES viewer, free embroidery tool, JEF viewer, EXP embroidery, VP3 embroidery viewer, embroidery preview tool, browser embroidery renderer, convert embroidery to PNG",
"seo.url": "https://embroideryviewer.xyz"
"seo.url": "https://embroideryviewer.xyz",
"seo.image": "https://embroideryviewer.xyz/og/"
}

View file

@ -1,10 +0,0 @@
{
"title": "Privacy Policy",
"last.update": "Last updated: Jul 11, 2025",
"content": "<h1>Privacy Policy</h1><p>Thank you for using the Embroidery Viewer Companion app. Your privacy is important to us. This Privacy Policy explains how we handle your data.</p><h2>1. No Personal Data Collected</h2><p>The app does not collect, store, or transmit any personal data. All your embroidery files are processed locally on your device. We do not access or send your files anywhere.</p><h2>2. Storage Permissions</h2><p>The app requests access to your files only so that you can open and view embroidery files stored on your device. This permission is used solely for that purpose.</p><h2>3. Name Personalization</h2><p>If you choose to enter your name, it is stored locally on your device to personalize greetings (like \"Good morning, Leo\"). This information is not shared and is never sent over the internet.</p><h2>4. No Analytics or Advertising</h2><p>This app does not use any analytics tools or display ads. We believe in a distraction-free and respectful experience.</p><h2>5. Questions</h2><p>If you have any questions about this policy or the app, feel free to contact us at: <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a></p>",
"seo.title": "Privacy Policy - Embroidery Viewer Companion App",
"seo.description": "Learn how Embroidery Viewer Companion App respects your privacy. No personal data collected, files processed locally or temporarily, anonymous analytics only, no trackers used.",
"seo.keywords": "privacy policy, data protection, embroidery viewer privacy, file uploads privacy, anonymous analytics, no cookies, user privacy, privacy-friendly analytics, data security, embroideryviewer.xyz",
"seo.url": "https://embroideryviewer.xyz/mobile-app/privacy-policy",
"seo.image": "https://embroideryviewer.xyz/og/privacy-policy.png"
}

View file

@ -1,17 +0,0 @@
{
"title": {
"prefix": "Embroidery Viewer on ",
"highlight": "Android"
},
"description": "Visualize and explore your embroidery files directly on your phone — fast, simple, and built for real workflows.",
"features": {
"formats": "Supports PES, JEF, PEC, VP3, DST and EXP formats",
"customization": "Customize background and thread colors",
"highlight": "Tap to highlight thread sequences",
"metadata": "View stitches, size and color breakdown",
"performance": "Fast, lightweight and works offline",
"accessibility": "Accessible and easy to use"
},
"cta_alt": "Download on Google Play",
"image_alt": "Android Embroidery Viewer preview"
}

View file

@ -1,8 +1,8 @@
{
"title": "Privacy Policy",
"last.update": "Last updated: Apr 24, 2026",
"content": "<p>At <strong>Embroidery Viewer</strong> (<a href=\"https://embroideryviewer.xyz\">embroideryviewer.xyz</a>), we respect your privacy and are committed to protecting any information you share while using our service.</p><h2>1. Personal Information</h2><p>Embroidery Viewer does <strong>not</strong> collect or store any personal information. You do not need to create an account, and we do not ask for your name, email address, or any identifying details.</p><h2>2. File Uploads</h2><p>When you upload an embroidery file to the viewer, the file is processed in your browser or temporarily on our server (if required) for preview purposes only. <strong>No uploaded files are stored, saved, or shared.</strong></p><p>Please avoid uploading any copyrighted or sensitive material unless you have permission to use it.</p><h2>3. Analytics</h2><p>We use <strong>HitKeep</strong> to collect anonymous usage statistics about our website, such as the number of visitors, page views, device types, and referral sources. This data helps us understand how the site is being used and improve it over time.</p><p>HitKeep is a privacy-friendly, cookie-free analytics tool. It does <strong>not</strong> track users across sites, collect personal data, or use cookies. All data is aggregated and anonymized.</p><h2>4. Cookies</h2><p>Embroidery Viewer does <strong>not</strong> use cookies or other tracking mechanisms in your browser.</p><h2>5. Third-Party Services</h2><p>We do not use third-party advertising, embed external trackers, or share data with third parties.</p><h2>6. Changes to This Policy</h2><p>We may update this Privacy Policy from time to time. All updates will be posted on this page with the updated date.</p><h2>7. Contact</h2><p>If you have any questions about this Privacy Policy, you can reach us at <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a>.</p>",
"seo.title": "Privacy Policy - Embroidery Viewer",
"title": "🔐 Privacy Policy",
"last.update": "Last updated: May 9, 2025",
"content": "<p>At <strong>Embroidery Viewer</strong> (<a href=\"https://embroideryviewer.xyz\">embroideryviewer.xyz</a>), we respect your privacy and are committed to protecting any information you share while using our service.</p><h2>1. Personal Information</h2><p>Embroidery Viewer does <strong>not</strong> collect or store any personal information. You do not need to create an account, and we do not ask for your name, email address, or any identifying details.</p><h2>2. File Uploads</h2><p>When you upload an embroidery file to the viewer, the file is processed in your browser or temporarily on our server (if required) for preview purposes only. <strong>No uploaded files are stored, saved, or shared.</strong></p><p>Please avoid uploading any copyrighted or sensitive material unless you have permission to use it.</p><h2>3. Analytics</h2><p>We use <strong>Umami</strong> to collect anonymous usage statistics about our website, such as the number of visitors, page views, device types, and referral sources. This data helps us understand how the site is being used and improve it over time.</p><p>Umami is a privacy-friendly, cookie-free analytics tool. It does <strong>not</strong> track users across sites, collect personal data, or use cookies. All data is aggregated and anonymized.</p><h2>4. Cookies</h2><p>Embroidery Viewer does <strong>not</strong> use cookies or other tracking mechanisms in your browser.</p><h2>5. Third-Party Services</h2><p>We do not use third-party advertising, embed external trackers, or share data with third parties.</p><h2>6. Changes to This Policy</h2><p>We may update this Privacy Policy from time to time. All updates will be posted on this page with the updated date.</p><h2>7. Contact</h2><p>If you have any questions about this Privacy Policy, you can reach us at <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a>.</p>",
"seo.title": "🔐 Privacy Policy - Embroidery Viewer",
"seo.description": "Learn how Embroidery Viewer respects your privacy. No personal data collected, files processed locally or temporarily, anonymous analytics only, no cookies or trackers used.",
"seo.keywords": "privacy policy, data protection, embroidery viewer privacy, file uploads privacy, anonymous analytics, no cookies, user privacy, privacy-friendly analytics, data security, embroideryviewer.xyz",
"seo.url": "https://embroideryviewer.xyz/privacy-policy",

View file

@ -1,10 +0,0 @@
{
"title": "Keep embroidery simple for everyone",
"description": "Embroidery Viewer saves you time and removes the hassle of dealing with complex software. Your support helps keep it <strong>free, fast, and continuously improving</strong> for everyone.",
"cta": "Support the project",
"seo.title": "Support Embroidery Viewer",
"seo.description": "Help keep Embroidery Viewer free and improving by making a donation. Choose from Bitcoin, Monero, PayPal, or other secure options to support ongoing development and hosting.",
"seo.keywords": "donate embroidery viewer, support embroidery viewer, embroidery viewer donations, help embroidery viewer, fund embroidery viewer, bitcoin donation embroidery, monero donation embroidery, paypal donation embroidery",
"url": "https://embroideryviewer.xyz/support-us",
"image": "https://embroideryviewer.xyz/og/donate.png"
}

View file

@ -1,8 +1,8 @@
{
"title": "Terms of Service",
"title": "📝 Terms of Service",
"update": "May 9, 2025",
"content": "<p>Welcome to <strong>Embroidery Viewer</strong> (<a href=\"https://embroideryviewer.xyz\">embroideryviewer.xyz</a>). By accessing or using this website, you agree to be bound by the following Terms of Service. If you do not agree with any part of these terms, please do not use the site.</p><h2>1. Description of Service</h2><p>Embroidery Viewer is a free, browser-based tool that allows users to preview embroidery design files online. The service is intended for personal, non-commercial use.</p><h2>2. Use of the Service</h2><p>You agree to use the service only for lawful purposes. You are solely responsible for any content (including embroidery files) you upload, and you confirm that you have the legal right to use, view, and process those files.</p><p>You agree not to upload any files that are illegal, offensive, infringe on intellectual property rights, or contain malicious code.</p><h2>3. File Processing</h2><p>Files uploaded to Embroidery Viewer are processed either directly in your browser or temporarily on our servers. Files are not stored permanently, shared, or backed up.</p><p>While we aim to keep your content secure, you acknowledge that no system is 100% secure and you use the service at your own risk.</p><h2>4. No Warranty</h2><p>This service is provided \"as is\" and \"as available\" without any warranties, express or implied. We do not guarantee that the service will be uninterrupted, secure, or error-free.</p><h2>5. Limitation of Liability</h2><p>Embroidery Viewer shall not be held liable for any damages resulting from the use or inability to use the service, including but not limited to loss of data, loss of profits, or other incidental or consequential damages.</p><h2>6. Modifications to the Service</h2><p>We reserve the right to modify, suspend, or discontinue the service at any time without notice. We may also update these Terms of Service from time to time. Continued use of the service after changes constitutes your acceptance of the new terms.</p><h2>7. Governing Law</h2><p>These Terms shall be governed by and interpreted in accordance with the laws of Brazil, without regard to its conflict of law principles.</p><h2>8. Contact</h2><p>If you have any questions about these Terms of Service, feel free to contact us at <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a>.</p>",
"seo.title": "Terms of Service - Embroidery Viewer",
"seo.title": "📝 Terms of Service - Embroidery Viewer",
"seo.description": "Read the Terms of Service for Embroidery Viewer. Personal use, upload rules, file processing, warranty disclaimers, liability limitations, and governing law.",
"seo.keywords": "terms of service, terms of use, personal use, file upload, file processing, warranty disclaimer, liability limitation, Brazilian law, embroideryviewer.xyz",
"seo.url": "https://embroideryviewer.xyz/terms-of-service",

View file

@ -11,7 +11,7 @@
"dimensions": "Dimensions (x, y)",
"download": "Download image",
"warning.copyright": "Do not upload copyrighted material you do not own or have rights to.",
"seo.title": "Free Online Embroidery File Viewer Fast, Private & No Signup",
"seo.title": "🧵 Free Online Embroidery File Viewer Fast, Private & No Signup",
"seo.description": "Upload and preview your embroidery files instantly with Embroidery Viewer. Supports DST, PES, JEF, EXP, VP3, and more. No installs, no uploads 100% browser-based and free.",
"seo.keywords": "embroidery viewer, online embroidery viewer, embroidery file preview, DST viewer, PES viewer, free embroidery tool, JEF viewer, EXP embroidery, VP3 embroidery viewer, embroidery preview tool, browser embroidery renderer, convert embroidery to PNG",
"seo.url": "https://embroideryviewer.xyz/viewer",

View file

@ -16,7 +16,7 @@ export const SUPPORTED_LOCALES = Object.freeze({
/** @type {import('sveltekit-i18n').Config} */
const config = {
initLocale: SUPPORTED_LOCALES.EN_US,
initLocale: navigator.language,
fallbackLocale: SUPPORTED_LOCALES.EN_US,
loaders: [
{
@ -43,9 +43,9 @@ const config = {
},
{
locale: SUPPORTED_LOCALES.EN_US,
key: 'support-us',
routes: ['/support-us'],
loader: async () => (await import('./en-US/support-us.json')).default,
key: 'donate',
routes: ['/donate'],
loader: async () => (await import('./en-US/donate.json')).default,
},
{
locale: SUPPORTED_LOCALES.EN_US,
@ -53,13 +53,6 @@ const config = {
routes: ['/privacy-policy'],
loader: async () => (await import('./en-US/privacy-policy.json')).default,
},
{
locale: SUPPORTED_LOCALES.EN_US,
key: 'mobile.app.privacy.policy',
routes: ['/mobile-app/privacy-policy'],
loader: async () =>
(await import('./en-US/mobile-app-privacy-policy.json')).default,
},
{
locale: SUPPORTED_LOCALES.EN_US,
key: 'terms.of.service',
@ -97,9 +90,9 @@ const config = {
},
{
locale: SUPPORTED_LOCALES.PT_BR,
key: 'support-us',
routes: ['/support-us'],
loader: async () => (await import('./pt-BR/support-us.json')).default,
key: 'donate',
routes: ['/donate'],
loader: async () => (await import('./pt-BR/donate.json')).default,
},
{
locale: SUPPORTED_LOCALES.PT_BR,
@ -107,13 +100,6 @@ const config = {
routes: ['/privacy-policy'],
loader: async () => (await import('./pt-BR/privacy-policy.json')).default,
},
{
locale: SUPPORTED_LOCALES.PT_BR,
key: 'mobile.app.privacy.policy',
routes: ['/mobile-app/privacy-policy'],
loader: async () =>
(await import('./pt-BR/mobile-app-privacy-policy.json')).default,
},
{
locale: SUPPORTED_LOCALES.PT_BR,
key: 'terms.of.service',
@ -127,56 +113,6 @@ const config = {
routes: ['/viewer'],
loader: async () => (await import('./pt-BR/viewer.json')).default,
},
{
locale: SUPPORTED_LOCALES.PT_BR,
key: 'hero',
loader: async () => (await import('./pt-BR/hero.json')).default,
},
{
locale: SUPPORTED_LOCALES.EN_US,
key: 'hero',
loader: async () => (await import('./en-US/hero.json')).default,
},
{
locale: SUPPORTED_LOCALES.PT_BR,
key: 'features',
loader: async () => (await import('./pt-BR/features.json')).default,
},
{
locale: SUPPORTED_LOCALES.EN_US,
key: 'features',
loader: async () => (await import('./en-US/features.json')).default,
},
{
locale: SUPPORTED_LOCALES.PT_BR,
key: 'faq',
loader: async () => (await import('./pt-BR/faq.json')).default,
},
{
locale: SUPPORTED_LOCALES.EN_US,
key: 'faq',
loader: async () => (await import('./en-US/faq.json')).default,
},
{
locale: SUPPORTED_LOCALES.PT_BR,
key: 'mobile',
loader: async () => (await import('./pt-BR/mobile.json')).default,
},
{
locale: SUPPORTED_LOCALES.EN_US,
key: 'mobile',
loader: async () => (await import('./en-US/mobile.json')).default,
},
{
locale: SUPPORTED_LOCALES.PT_BR,
key: 'announcement',
loader: async () => (await import('./pt-BR/announcement.json')).default,
},
{
locale: SUPPORTED_LOCALES.EN_US,
key: 'announcement',
loader: async () => (await import('./en-US/announcement.json')).default,
},
],
};
@ -191,10 +127,7 @@ export const {
} = new i18n(config);
locale.subscribe(($locale) => {
if (typeof localStorage !== 'undefined' && $locale) {
const existing = localStorage.getItem('locale');
if (existing !== $locale) {
localStorage.setItem('locale', $locale);
}
if (typeof document !== 'undefined') {
document.cookie = `locale=${$locale}; path=/; SameSite=Strict;`;
}
});

View file

@ -1,34 +1,9 @@
{
"seo.title": "Sobre o Embroidery Viewer - Por que esta ferramenta foi criada",
"title": " Sobre o Embroidery Viewer",
"content": "<p>Oi! 👋</p><p><strong>⭐️ Embroidery Viewer</strong> nasceu de uma necessidade simples — ajudar alguém que eu amo. 💖</p><p>Minha namorada adora bordado, mas ela sempre teve dificuldades para encontrar uma maneira fácil e gratuita de visualizar os arquivos de design de bordado antes de começar a costurar. A maioria das ferramentas que ela tentou eram pagas, muito complexas ou exigiam conhecimento técnico — e ela não é da área de tecnologia.</p><p>Então, para facilitar a vida dela (e de outras pessoas como ela), decidi criar este aplicativo web.</p><p>Ao longo de algumas semanas, criei o <strong>Embroidery Viewer</strong> — uma ferramenta leve, rápida e gratuita que permite visualizar arquivos de bordado diretamente no navegador. Sem instalação, sem configuração e sem obstáculos técnicos. Basta enviar o arquivo e ver o design.</p><p>Não é uma ferramenta super sofisticada, mas resolve o problema para o qual foi criada: tornar a visualização de arquivos de bordado acessível para todos.</p><p>Se essa ferramenta também te ajudou, isso me deixa muito feliz! Pretendo continuar melhorando com base no feedback de usuários como você.</p><p>Obrigado por visitar — e bons bordados! 🧵✨</p>",
"seo.title": "Sobre o Embroidery Viewer - Por que esta ferramenta foi criada",
"seo.description": "Conheça a história por trás do Embroidery Viewer — uma ferramenta gratuita e online criada para tornar a visualização de arquivos de bordado simples, rápida e acessível a todos.",
"seo.keywords": "sobre embroidery viewer, história do embroidery viewer, visualizador de bordado gratuito, motivo da criação do embroidery viewer, quem criou o embroidery viewer, visualizador online de bordado, ferramenta gratuita para bordado, embroidery viewer sobre",
"seo.url": "https://embroideryviewer.xyz/about",
"seo.image": "https://embroideryviewer.xyz/og/about.png",
"hero": {
"tagline": "UM PEQUENO ATO DE AMOR",
"title": "Que se tornou uma forma poderosa de visualizar arquivos de bordado"
},
"story": {
"title": "A história por trás do Embroidery Viewer...",
"p1": "Não começou como um produto. Começou com uma frustração simples.",
"p2": "Alguém que eu amo gosta de bordado. Ela adora criar, explorar designs e dar vida às ideias através dos pontos. Mas toda vez que baixava um novo arquivo de bordado, enfrentava o mesmo problema:",
"quote1": "“Como eu abro isso?”",
"p3": "A maioria das ferramentas disponíveis eram pagas, complexas demais ou feitas para pessoas que já entendiam softwares técnicos de bordado. Para alguém que só queria visualizar rapidamente um design antes de bordar, era complicado demais.",
"p4": "O que deveria ser um passo simples — apenas ver o design — se tornava uma barreira. Foi aí que percebi algo importante:",
"quote2": "Provavelmente existem muitas pessoas passando exatamente pelo mesmo problema."
},
"product": {
"p1": "As pessoas não querem softwares complicados.",
"p2": "As pessoas não querem instalar nada.",
"p3": "As pessoas só querem abrir um arquivo de bordado e ver o design na hora.",
"p4": "Então, em vez de procurar uma ferramenta melhor…",
"quote": "Eu decidi criar uma."
},
"support": {
"title": "Se te ajudou, você pode ajudar a crescer",
"p1": "O Embroidery Viewer é completamente gratuito — e eu pretendo manter assim.",
"p2": "Mas manter ele rápido, melhorar o suporte a mais formatos e sustentar a infraestrutura exige tempo e recursos.",
"p3": "Se essa ferramenta facilitou seu fluxo, ajudou você a visualizar um design ou evitou trabalho desnecessário, você pode apoiar o futuro dela.",
"cta": "Apoiar agora"
}
"seo.image": "https://embroideryviewer.xyz/og/about.png"
}

View file

@ -1,4 +0,0 @@
{
"message": "🚀 Embroidery Viewer agora está no Android — visualize seus designs em qualquer lugar, personalize as cores das linhas e do fundo.",
"cta-text": "Instale agora"
}

View file

@ -0,0 +1,18 @@
{
"title": "💖 Doe",
"subtitle": "Ajude a apoiar o Embroidery Viewer e seu desenvolvimento!",
"description": "⭐️ O <strong>Embroidery Viewer</strong> é gratuito. Se você achar esta ferramenta útil, considere fazer uma doação para mantê-la funcionando e financiar melhorias futuras.",
"ways": "💸 Formas de doar",
"bitcoin.description": "Escaneie ou copie o endereço",
"copy": "Copiar Endereço",
"copied": "Copiado para a área de transferência!",
"copy.failed": "Falha na Cópia!",
"monero.description": "Opção de doação privada e segura.",
"paypal.description": "Quer demonstrar apoio de uma forma amigável?",
"paypal.link": "Abrir Link de Doação",
"seo.title": "💖 Doe Apoie o Embroidery Viewer",
"seo.description": "Ajude a manter o Embroidery Viewer gratuito e em constante melhoria fazendo uma doação. Escolha entre Bitcoin, Monero, PayPal ou outras opções seguras para apoiar o desenvolvimento e hospedagem.",
"seo.keywords": "doar embroidery viewer, apoie embroidery viewer, doações embroidery viewer, ajudar embroidery viewer, financiar embroidery viewer, doação bitcoin embroidery, doação monero embroidery, doação paypal embroidery",
"url": "https://embroideryviewer.xyz/doar",
"image": "https://embroideryviewer.xyz/og/doar.png"
}

View file

@ -1,30 +0,0 @@
{
"title": "Perguntas Frequentes",
"intro": "Saiba como abrir arquivos de bordado online, quais formatos são suportados e como o Embroidery Viewer funciona — tudo em um só lugar.",
"items": {
"openPesOnline": {
"summary": "Como posso abrir um arquivo PES online?",
"description": "Você pode abrir um arquivo PES instantaneamente usando o Embroidery Viewer. Basta arrastar e soltar o arquivo no navegador para visualizar o design de bordado — sem precisar instalar nenhum software."
},
"supportedFormats": {
"summary": "Quais formatos de arquivos de bordado são suportados?",
"description": "O Embroidery Viewer suporta formatos populares como PES, DST e EXP. Isso permite visualizar a maioria dos designs de bordado usados em máquinas domésticas e industriais."
},
"needSoftware": {
"summary": "Preciso instalar algum software de bordado?",
"description": "Não. O Embroidery Viewer funciona totalmente no navegador, permitindo abrir arquivos de bordado online sem precisar baixar ou instalar nada."
},
"isSafe": {
"summary": "É seguro usar o Embroidery Viewer?",
"description": "Sim. Seus arquivos são processados localmente no seu navegador e nunca são enviados para nenhum servidor, garantindo total privacidade."
},
"multipleFiles": {
"summary": "Posso visualizar vários arquivos de bordado ao mesmo tempo?",
"description": "Sim. Você pode abrir e comparar vários arquivos simultaneamente em uma única visualização, facilitando a análise e escolha dos designs."
},
"mobileSupport": {
"summary": "Posso usar no celular ou tablet?",
"description": "Sim. O Embroidery Viewer funciona em desktop, tablet e celular, desde que você utilize um navegador moderno."
}
}
}

View file

@ -1,27 +0,0 @@
{
"title": "A maneira mais fácil de visualizar arquivos de bordado",
"subtitle": "Seja você um entusiasta trabalhando no seu próximo projeto DIY ou um digitizador profissional revisando arquivos de clientes, o Embroidery Viewer oferece uma forma rápida e simples de visualizar designs de bordado online — sem software, sem complicação.",
"cards": {
"fast": {
"adjective": "RÁPIDO",
"title": "Visualização instantânea",
"description": "Arraste seu arquivo e veja o design na hora — sem instalar nada, sem esperar."
},
"private": {
"adjective": "PRIVADO",
"title": "Privacidade em primeiro lugar",
"description": "Seus arquivos ficam no seu dispositivo. Nada é enviado para a internet."
},
"optimized": {
"adjective": "OTIMIZADO",
"title": "Vários arquivos, uma visão",
"description": "Abra e compare vários designs ao mesmo tempo em um layout limpo."
},
"compatibility": {
"adjective": "COMPATÍVEL",
"title": "Suporta formatos populares",
"description": "Funciona com PES, DST, EXP e outros formatos comuns de bordado."
}
},
"cta": "Experimente agora"
}

View file

@ -1,15 +1,7 @@
{
"slogan": "Visualize bordados instantaneamente — sem software",
"resources": "Recursos",
"contact-title": "Contato",
"contact-description": "Quer ajudar ou tirar alguma dúvida? Contate-nos.",
"back-to-top": {
"label": "Voltar ao topo",
"aria-label": "Voltar ao topo da página"
},
"about": "Sobre",
"privacy.policy": "Política de Privacidade",
"terms.of.service": "Termos de Serviço",
"copyright": "Copyright © {{year}} <a href=\"{{website}}/pt-br\" target=\"_blank\" rel=\"noreferrer\">Leonardo Murça</a>. Todos os direitos reservados.",
"version": "Versão {{version}}"
"about": " Sobre",
"privacy.policy": "🔐 Política de Privacidade",
"terms.of.service": "📝 Termos de Serviço",
"copyright": "Copyright © {{year}} <a href=\"{{website}}/pt-br\" target=\"_blank\" rel=\"noreferrer\">Leonardo Murça</a>. <br/> Todos os direitos reservados.",
"version": "🧵 Versão: {{version}}"
}

View file

@ -1,6 +1,7 @@
{
"homeNav": "Página Inicial",
"aboutNav": "Sobre",
"viewerNav": "Visualizador",
"supportUsNav": "Apoie-nos"
"languageSwitch": "🇺🇸",
"homeNav": "🏠 Página Inicial",
"aboutNav": " Sobre",
"viewerNav": "🧵 Visualizador",
"donateNav": "💖 Doe"
}

View file

@ -1,5 +0,0 @@
{
"title": "Visualize bordados instantaneamente — sem software",
"description": "Rápido, privado & sem necessidade de cadastro.",
"cta": "Teste Agora"
}

View file

@ -1,6 +1,22 @@
{
"seo.title": "Visualizador de Bordado Online Grátis - Rápido, Privado e Sem Cadastro",
"main.title": "🧵 Visualizador de arquivos de bordado online gratuito",
"main.description": "<p>✨Carregue e visualize seus desenhos de bordado instantaneamente sem necessidade de software</p> <p><strong>Embroidery Viewer</strong> é uma ferramenta gratuita para navegador que suporta diversos formatos de arquivo de bordado. Visualize seus designs de forma rápida e segura, diretamente no seu navegador.</p>",
"features.title": "🚀 Funcionalidades",
"features.list": "<ul><li>📂 <strong>Suporta vários formatos:</strong> DST, PES, JEF, EXP, VP3 e mais</li><li>⚡ <strong>Visualizações rápidas:</strong> Veja seus arquivos de bordado renderizados como imagens</li><li>🧷 <strong>Vários arquivos de uma só vez:</strong> Carregue vários designs e visualize-os lado a lado</li><li>🔒 <strong>Sem upload para o servidor:</strong> Seus arquivos permanecem privados todo o processamento acontece localmente</li><li>⬇️ <strong>Baixar como imagem:</strong> Salve cada pré-visualização do desenho do bordado como um PNG</li><li>💸 <strong>Rápido e gratuito:</strong> Sem instalações, sem cadastros basta abrir e usar</li></ul>",
"howtouse.title": "📘 Como usar",
"howtouse.list": "<ol><li>📁 <strong>Clique</strong> no botão de upload <em>ou</em> <strong>arraste e solte</strong> seus arquivos de bordado na área de soltar</li><li>🧵 Selecione um ou mais arquivos de bordado</li><li>▶️ Clique no botão <strong>“Renderizar arquivos”</strong> para visualizar seus designs</li><li>👀 Visualize seus designs instantaneamente no seu navegador é simples assim</li></ol>",
"testimonials.title": "❤️ Amado por Hobbyistas e Profissionais",
"testimonials.description": "<p>Seja você um amador trabalhando em seu próximo projeto \"faça você mesmo\" ou um digitalizador profissional revisando arquivos de clientes, o <strong>Embroidery Viewer</strong> oferece uma maneira fácil e instantânea de visualizar seu trabalho.</p>",
"donation.title": "💖 Ajude a mantê-lo gratuito",
"donation.description": "<p><strong>O Embroidery Viewer é totalmente gratuito</strong> para todos usarem.</p><p>Se você o achar útil e quiser apoiar o desenvolvimento contínuo e os custos de hospedagem, considere fazer uma pequena doação.</p>",
"donation.cta": "🙌 Doe agora",
"donation.cta.description": "cada pequena ajuda é bem-vinda!",
"cta.title": "🚀 Experimente agora",
"cta.cta": "🧵 Abrir visualizador",
"cta.cta.description": "o <strong>visualizador de arquivos de bordado online gratuito</strong> mais rápido.",
"seo.title": "🏠 Visualizador de Bordado Online Grátis - Rápido, Privado e Sem Cadastro",
"seo.description": "Envie e visualize arquivos de bordado instantaneamente com o Embroidery Viewer. Compatível com DST, PES, JEF, EXP, VP3 e mais. Sem instalações, sem uploads 100% no navegador e gratuito.",
"seo.keywords": "visualizador de bordado, visualizador online de bordado, visualizar arquivos de bordado, visualizar DST, visualizar PES, ferramenta gratuita de bordado, visualizador JEF, bordado EXP, visualizador VP3, pré-visualização de bordado, renderizador de bordado no navegador, converter bordado em PNG",
"seo.url": "https://embroideryviewer.xyz"
"seo.url": "https://embroideryviewer.xyz",
"seo.image": "https://embroideryviewer.xyz/og/"
}

View file

@ -1,10 +0,0 @@
{
"title": "Política de Privacidade",
"last.update": "Última atualização: 11 de julho de 2025",
"content": "<h1>Política de Privacidade</h1><p>Obrigado por usar o aplicativo Embroidery Viewer Companion. Sua privacidade é importante para nós. Esta Política de Privacidade explica como lidamos com seus dados.</p><h2>1. Nenhum dado pessoal coletado</h2><p>O aplicativo não coleta, armazena ou transmite nenhum dado pessoal. Todos os seus arquivos de bordado são processados localmente no seu dispositivo. Não acessamos nem enviamos seus arquivos para lugar algum.</p><h2>2. Permissões de armazenamento</h2><p>O aplicativo solicita acesso aos seus arquivos apenas para que você possa abrir e visualizar arquivos de bordado armazenados no seu dispositivo. Essa permissão é usada exclusivamente para esse fim.</p><h2>3. Personalização com nome</h2><p>Se você optar por informar seu nome, ele será armazenado localmente no seu dispositivo para personalizar as saudações (como \"Bom dia, Leo\"). Esta informação não é compartilhada e nunca é enviada pela internet.</p><h2>4. Sem análises ou anúncios</h2><p>Este aplicativo não utiliza ferramentas de análise nem exibe anúncios. Acreditamos em uma experiência respeitosa e sem distrações.</p><h2>5. Dúvidas</h2><p>Se você tiver alguma dúvida sobre esta política ou sobre o aplicativo, entre em contato conosco pelo e-mail: <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a></p>",
"seo.title": "Política de Privacidade - Embroidery Viewer Companion App",
"seo.description": "Saiba como o Embroidery Viewer respeita sua privacidade. Nenhum dado pessoal é coletado, arquivos processados localmente ou temporariamente, análises anônimas, sem cookies ou rastreadores.",
"seo.keywords": "política de privacidade, proteção de dados, privacidade embroidery viewer, upload de arquivos, análises anônimas, sem cookies, privacidade do usuário, análises que respeitam a privacidade, segurança de dados, embroideryviewer.xyz",
"seo.url": "https://embroideryviewer.xyz/mobile-app/privacy-policy",
"seo.image": "https://embroideryviewer.xyz/og/privacy-policy.png"
}

View file

@ -1,17 +0,0 @@
{
"title": {
"prefix": "Embroidery Viewer no ",
"highlight": "Android"
},
"description": "Visualize e explore seus arquivos de bordado diretamente no celular — rápido, simples e feito para o uso real.",
"features": {
"formats": "Suporte para formatos PES, JEF, PEC, VP3, DST e EXP",
"customization": "Personalize cores de fundo e das linhas",
"highlight": "Toque para destacar sequências de cores",
"metadata": "Veja pontos, tamanho e detalhes das cores",
"performance": "Rápido, leve e funciona offline",
"accessibility": "Acessível e fácil de usar"
},
"cta_alt": "Baixar na Google Play",
"image_alt": "Prévia do Embroidery Viewer no Android"
}

View file

@ -1,8 +1,8 @@
{
"title": "Política de Privacidade",
"last.update": "Última atualização: 24 de abril de 2026",
"content": "<p>No <strong>Embroidery Viewer</strong> (<a href=\"https://embroideryviewer.xyz\">embroideryviewer.xyz</a>), respeitamos sua privacidade e estamos comprometidos em proteger qualquer informação que você compartilhe ao usar nosso serviço.</p><h2>1. Informações Pessoais</h2><p>O Embroidery Viewer <strong>não</strong> coleta nem armazena informações pessoais. Você não precisa criar uma conta e não pedimos seu nome, e-mail ou qualquer dado identificável.</p><h2>2. Envio de Arquivos</h2><p>Quando você envia um arquivo de bordado para o visualizador, o arquivo é processado no seu navegador ou temporariamente em nosso servidor (se necessário) apenas para fins de visualização. <strong>Nenhum arquivo enviado é armazenado, salvo ou compartilhado.</strong></p><p>Evite enviar materiais sensíveis ou protegidos por direitos autorais, a menos que tenha permissão para usá-los.</p><h2>3. Análises</h2><p>Utilizamos o <strong>HitKeep</strong> para coletar estatísticas anônimas de uso do site, como número de visitantes, visualizações de página, tipos de dispositivo e fontes de acesso. Esses dados nos ajudam a entender como o site está sendo utilizado e melhorá-lo com o tempo.</p><p>O HitKeep é uma ferramenta de análise que respeita a privacidade, não usa cookies e não rastreia os usuários entre sites. Todos os dados são agregados e anonimizados.</p><h2>4. Cookies</h2><p>O Embroidery Viewer <strong>não</strong> utiliza cookies ou outros mecanismos de rastreamento em seu navegador.</p><h2>5. Serviços de Terceiros</h2><p>Não utilizamos publicidade de terceiros, nem incorporamos rastreadores externos, nem compartilhamos dados com terceiros.</p><h2>6. Alterações nesta Política</h2><p>Podemos atualizar esta Política de Privacidade ocasionalmente. Todas as atualizações serão publicadas nesta página com a data de modificação.</p><h2>7. Contato</h2><p>Se você tiver dúvidas sobre esta Política de Privacidade, entre em contato pelo e-mail <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a>.</p>",
"seo.title": "Política de Privacidade - Embroidery Viewer",
"title": "🔐 Política de Privacidade",
"last.update": "Última atualização: 9 de maio de 2025",
"content": "<p>No <strong>Embroidery Viewer</strong> (<a href=\"https://embroideryviewer.xyz\">embroideryviewer.xyz</a>), respeitamos sua privacidade e estamos comprometidos em proteger qualquer informação que você compartilhe ao usar nosso serviço.</p><h2>1. Informações Pessoais</h2><p>O Embroidery Viewer <strong>não</strong> coleta nem armazena informações pessoais. Você não precisa criar uma conta e não pedimos seu nome, e-mail ou qualquer dado identificável.</p><h2>2. Envio de Arquivos</h2><p>Quando você envia um arquivo de bordado para o visualizador, o arquivo é processado no seu navegador ou temporariamente em nosso servidor (se necessário) apenas para fins de visualização. <strong>Nenhum arquivo enviado é armazenado, salvo ou compartilhado.</strong></p><p>Evite enviar materiais sensíveis ou protegidos por direitos autorais, a menos que tenha permissão para usá-los.</p><h2>3. Análises</h2><p>Utilizamos o <strong>Umami</strong> para coletar estatísticas anônimas de uso do site, como número de visitantes, visualizações de página, tipos de dispositivo e fontes de acesso. Esses dados nos ajudam a entender como o site está sendo utilizado e melhorá-lo com o tempo.</p><p>O Umami é uma ferramenta de análise que respeita a privacidade, não usa cookies e não rastreia os usuários entre sites. Todos os dados são agregados e anonimizados.</p><h2>4. Cookies</h2><p>O Embroidery Viewer <strong>não</strong> utiliza cookies ou outros mecanismos de rastreamento em seu navegador.</p><h2>5. Serviços de Terceiros</h2><p>Não utilizamos publicidade de terceiros, nem incorporamos rastreadores externos, nem compartilhamos dados com terceiros.</p><h2>6. Alterações nesta Política</h2><p>Podemos atualizar esta Política de Privacidade ocasionalmente. Todas as atualizações serão publicadas nesta página com a data de modificação.</p><h2>7. Contato</h2><p>Se você tiver dúvidas sobre esta Política de Privacidade, entre em contato pelo e-mail <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a>.</p>",
"seo.title": "🔐 Política de Privacidade - Embroidery Viewer",
"seo.description": "Saiba como o Embroidery Viewer respeita sua privacidade. Nenhum dado pessoal é coletado, arquivos processados localmente ou temporariamente, análises anônimas, sem cookies ou rastreadores.",
"seo.keywords": "política de privacidade, proteção de dados, privacidade embroidery viewer, upload de arquivos, análises anônimas, sem cookies, privacidade do usuário, análises que respeitam a privacidade, segurança de dados, embroideryviewer.xyz",
"seo.url": "https://embroideryviewer.xyz/privacy-policy",

View file

@ -1,10 +0,0 @@
{
"title": "Mantenha o bordado simples para todos",
"description": "O Embroidery Viewer economiza seu tempo e elimina a complexidade de softwares complicados. Seu apoio ajuda a mantê-lo <strong>gratuito, rápido e em constante evolução</strong> para todos.",
"cta": "Apoiar o projeto",
"seo.title": "Apoie o Embroidery Viewer",
"seo.description": "Ajude a manter o Embroidery Viewer gratuito e em constante evolução fazendo uma doação. Escolha entre Bitcoin, Monero, PayPal ou outras opções seguras para apoiar o desenvolvimento e a infraestrutura.",
"seo.keywords": "doar embroidery viewer, apoiar embroidery viewer, doações embroidery viewer, ajudar embroidery viewer, financiar embroidery viewer, doação bitcoin bordado, doação monero bordado, doação paypal bordado",
"url": "https://embroideryviewer.xyz/support-us",
"image": "https://embroideryviewer.xyz/og/donate.png"
}

View file

@ -1,8 +1,8 @@
{
"title": "Termos de Serviço",
"title": "📝 Termos de Serviço",
"update": "Última atualização: 9 de maio de 2025",
"content": "<p>Bem-vindo ao <strong>Embroidery Viewer</strong> (<a href=\"https://embroideryviewer.xyz\">embroideryviewer.xyz</a>). Ao acessar ou utilizar este site, você concorda em estar vinculado aos seguintes Termos de Serviço. Se você não concordar com qualquer parte destes termos, por favor, não utilize o site.</p><h2>1. Descrição do Serviço</h2><p>O Embroidery Viewer é uma ferramenta gratuita baseada em navegador que permite aos usuários visualizar arquivos de design de bordado online. O serviço é destinado ao uso pessoal e não comercial.</p><h2>2. Uso do Serviço</h2><p>Você concorda em usar o serviço apenas para fins legais. Você é o único responsável por qualquer conteúdo (incluindo arquivos de bordado) que enviar, e confirma que tem o direito legal de usar, visualizar e processar esses arquivos.</p><p>Você concorda em não enviar arquivos que sejam ilegais, ofensivos, infrinjam direitos de propriedade intelectual ou contenham código malicioso.</p><h2>3. Processamento de Arquivos</h2><p>Os arquivos enviados para o Embroidery Viewer são processados diretamente em seu navegador ou temporariamente em nossos servidores. Os arquivos não são armazenados permanentemente, compartilhados ou backupados.</p><p>Embora tenhamos o objetivo de manter seu conteúdo seguro, você reconhece que nenhum sistema é 100% seguro e você utiliza o serviço por sua conta e risco.</p><h2>4. Sem Garantia</h2><p>Este serviço é fornecido \"como está\" e \"como disponível\", sem quaisquer garantias, expressas ou implícitas. Não garantimos que o serviço será ininterrupto, seguro ou sem erros.</p><h2>5. Limitação de Responsabilidade</h2><p>O Embroidery Viewer não será responsabilizado por quaisquer danos resultantes do uso ou da impossibilidade de usar o serviço, incluindo, mas não se limitando a, perda de dados, perda de lucros ou outros danos incidentais ou consequenciais.</p><h2>6. Modificações no Serviço</h2><p>Reservamo-nos o direito de modificar, suspender ou descontinuar o serviço a qualquer momento, sem aviso prévio. Podemos também atualizar estes Termos de Serviço de tempos em tempos. O uso contínuo do serviço após as mudanças constitui sua aceitação dos novos termos.</p><h2>7. Lei Aplicável</h2><p>Estes Termos serão regidos e interpretados de acordo com as leis do Brasil, sem levar em consideração seus princípios de conflitos de leis.</p><h2>8. Contato</h2><p>Se você tiver qualquer dúvida sobre estes Termos de Serviço, sinta-se à vontade para entrar em contato conosco pelo e-mail <a href=\"mailto:leo@leomurca.xyz\">leo@leomurca.xyz</a>.</p>",
"seo.title": "Termos de Serviço - Embroidery Viewer",
"seo.title": "📝 Termos de Serviço - Embroidery Viewer",
"seo.description": "Leia os Termos de Serviço do Embroidery Viewer. Uso pessoal, regras de upload, processamento de arquivos, isenção de garantias, limitações de responsabilidade e legislação aplicável.",
"seo.keywords": "termos de serviço, condições de uso, uso pessoal, upload de arquivos, processamento de arquivos, isenção de garantias, limitações de responsabilidade, legislação brasileira, embroideryviewer.xyz",
"seo.url": "https://embroideryviewer.xyz/termos-de-servico",

View file

@ -1,5 +1,6 @@
{
"title": "Carregar arquivos",
"languageSwitch": "🇺🇸",
"fileSize": "O tamanho máximo de cada arquivo é <strong>{{fileSize}}MB</strong>.",
"supportedFormats": "Formatos aceitos: <strong>{{supportedFormats}}</strong>.",
"render": "Renderizar arquivos",
@ -8,10 +9,9 @@
"selected": "Arquivos selecionados",
"rejected": "Arquivos recusados",
"stitches": "Pontos",
"dimensions": "Dimensões (x, y)",
"download": "Baixar imagem",
"warning.copyright": "Não carregue material protegido por direitos autorais que você não possui ou sobre os quais não tenha direitos.",
"seo.title": "Visualizador Online Gratuito de Arquivos de Bordado Rápido, Privado e Sem Cadastro",
"seo.title": "🧵 Visualizador Online Gratuito de Arquivos de Bordado Rápido, Privado e Sem Cadastro",
"seo.description": "Faça upload e visualize seus arquivos de bordado instantaneamente com o Embroidery Viewer. Suporta DST, PES, JEF, EXP, VP3 e muito mais. Sem instalações, sem upload para servidor 100% baseado no navegador e gratuito.",
"seo.keywords": "visualizador de bordado, visualizador online de bordado, pré-visualização de arquivos de bordado, visualizador DST, visualizador PES, ferramenta gratuita de bordado, visualizador JEF, bordado EXP, visualizador VP3, ferramenta de pré-visualização de bordado, renderizador de bordado no navegador, converter bordado para PNG",
"seo.url": "https://embroideryviewer.xyz/viewer",

View file

@ -4,8 +4,4 @@ function appVersion() {
return APP_VERSION;
}
function isDevelopment() {
return import.meta.env.MODE === 'development';
}
export { appVersion, isDevelopment };
export { appVersion };

View file

@ -1,9 +0,0 @@
import { browser } from '$app/environment';
export const isMobile = () => {
if (browser) {
return window.matchMedia('only screen and (max-width: 768px)').matches;
} else {
return null;
}
};

View file

@ -1,15 +0,0 @@
/**
* Converts a locale string from hyphen format (e.g., "en-US")
* to underscore format (e.g., "en_US").
*
* Useful for APIs or systems that expect locales with underscores.
*
* @param {string} locale - The locale string in BCP 47 format (e.g., "en-US").
* @returns {string} The normalized locale string using underscores (e.g., "en_US").
*
* @example
* normalizeLocaleUnderscore("en-US"); // "en_US"
*/
export const normalizeLocaleUnderscore = (locale) => {
return locale.split('-').join('_');
};

View file

@ -1,51 +1,17 @@
import { browser } from '$app/environment';
import { loadTranslations, setLocale, setRoute } from '$lib/translations';
import { SUPPORTED_LOCALES } from '$lib/translations';
import { setLocale, setRoute } from '$lib/translations';
/**
* Type guard that checks if a string is a supported locale.
* @param {string | null} locale
* @returns {locale is "en-US" | "pt-BR"}
* @typedef {Object} LayoutData
* @property {string} route
* @property {string} language
*/
function isSupportedLocale(locale) {
// @ts-ignore
return locale !== null && Object.values(SUPPORTED_LOCALES).includes(locale);
}
/**
* Client-side load function to initialize translations based on localStorage or server fallback.
*
* This function runs in the browser and:
* - Prioritizes locale from localStorage (if valid)
* - Falls back to the server-provided fallbackLanguage (from Accept-Language)
* - Initializes translations and routing
*
* @type {import('@sveltejs/kit').Load}
*/
export const load = async ({ data, url }) => {
/** @type {string} */
const route = url.pathname;
/** @type {import('@sveltejs/kit').Load<LayoutData>} */
export const load = async ({ data }) => {
const { route, language } = data ?? {};
/** @type {"en-US" | "pt-BR"} */
let language;
if (route) await setRoute(route);
if (language) await setLocale(language);
if (browser) {
/**
* Locale stored in the browser, if any.
* @type {string | null}
*/
const stored = localStorage.getItem('locale');
if (isSupportedLocale(stored)) {
language = stored; // Type narrowed here
} else {
language = data?.fallbackLanguage ?? SUPPORTED_LOCALES.EN_US;
}
await loadTranslations(language, route);
await setLocale(language);
await setRoute(route);
}
return {};
return data ?? {};
};

View file

@ -1,44 +1,54 @@
import { parse } from 'accept-language-parser';
import { loadTranslations, setLocale, setRoute } from '$lib/translations';
import { SUPPORTED_LOCALES } from '$lib/translations';
/**
* A Set of all supported locale codes for quick validation.
* A set of all supported locale codes, used to validate and match against
* user preferences from cookies or Accept-Language headers. We're using a
* Set for better performance in lookup.
*
* Example values: "en-US", "pt-BR"
* @type {Set<string>}
*/
const SUPPORTED_LOCALE_SET = new Set(Object.values(SUPPORTED_LOCALES));
/**
* Extracts the best matching locale from an Accept-Language HTTP header.
*
* @param {string | null} header - The Accept-Language header value.
* @returns {string | null} - A supported locale string like "en-US", or null if none matched.
* Returns a valid locale from cookies, or null if not valid/found.
* @param {{ get: (cookies: string) => any; }} cookies
*/
function localeFromHeader(header) {
if (!header) return null;
const parsed = parse(header);
for (const { code, region } of parsed) {
const locale = region ? `${code}-${region}` : code;
if (SUPPORTED_LOCALE_SET.has(locale)) return locale;
}
return null;
function localeFromCookies(cookies) {
const locale = cookies.get('locale');
return locale && SUPPORTED_LOCALE_SET.has(locale) ? locale : null;
}
/**
* Server-side load function that returns the initial route and fallback language.
* The language is inferred from the Accept-Language header.
* `localStorage` will take precedence on the client.
*
* @type {import('@sveltejs/kit').ServerLoad}
* Parses the Accept-Language header and returns the best matching locale.
* @param {string | null | undefined} header
*/
export async function load({ url, request }) {
/** @type {string} */
function localeFromHeader(header) {
if (!header) return null;
const parsedLanguages = parse(header);
for (const { code, region } of parsedLanguages) {
const locale = region ? `${code}-${region}` : code;
if (SUPPORTED_LOCALE_SET.has(locale)) {
return locale;
}
}
return null;
}
/** @type {import('@sveltejs/kit').ServerLoad}*/
export async function load({ url, request, cookies }) {
const cookieLocale = localeFromCookies(cookies);
const headerLocale = localeFromHeader(request.headers.get('accept-language'));
const language = cookieLocale || headerLocale || SUPPORTED_LOCALES.EN_US;
const route = url.pathname;
/** @type {string} */
const fallbackLanguage =
localeFromHeader(request.headers.get('accept-language')) ||
SUPPORTED_LOCALES.EN_US;
await loadTranslations(language, route);
setLocale(language);
setRoute(route);
return { fallbackLanguage, route };
return { language, route };
}

View file

@ -1,43 +1,18 @@
<script>
import '$lib/styles/fonts.css';
import '$lib/styles/variables.css';
import '$lib/styles/global.css';
import { browser } from '$app/environment';
import { onMount } from 'svelte';
import Header from '$lib/components/Header.svelte';
import Footer from '$lib/components/Footer.svelte';
import Analytics from '$lib/components/Analytics.svelte';
import AnnouncementBar from '$lib/components/AnnouncementBar.svelte';
let { children } = $props();
let mounted = $state(false);
if (browser) {
onMount(() => {
mounted = true;
});
}
</script>
<Analytics />
{#if mounted}
<AnnouncementBar />
<Header />
<main>
{@render children()}
</main>
<Footer />
{/if}
<Header />
<main>
<slot />
</main>
<Footer />
<style>
main {
flex: 1; /* This pushes footer to bottom */
padding: 0;
padding: 20px;
min-height: 90vh;
overflow-x: hidden;
}
</style>

12
src/routes/+page.js Normal file
View file

@ -0,0 +1,12 @@
/** @type {import('./$types').PageLoad} */
export function load() {
return {
metadata: {
title: 'home.seo.title',
description: 'home.seo.description',
keywords: 'home.seo.keywords',
url: 'home.seo.url',
image: 'home.seo.image',
},
};
}

View file

@ -1,33 +0,0 @@
import { EMAIL_ACCESS_KEY, EMAIL_BASE_URL } from '$env/static/private';
/** @type {import('./$types').Actions} */
export const actions = {
default: async ({ request }) => {
const formData = await request.formData();
console.log(formData);
const response = await fetch(`${EMAIL_BASE_URL}/submit`, {
method: 'POST',
body: JSON.stringify({
accessKey: EMAIL_ACCESS_KEY,
subject: 'Contato - Embroidery Viewer Beta Testers!',
name: formData.get('name'),
email: formData.get('email'),
}),
headers: { 'Content-Type': 'application/json' },
});
const json = await response.json();
if (json.error === undefined) {
return {
message: 'home.banner.feedback.success',
textColor: 'green',
};
} else {
return {
message: 'home.banner.feedback.error',
textColor: 'red',
};
}
},
};

View file

@ -1,19 +1,64 @@
<script>
import Head from '$lib/components/Head.svelte';
import Hero from '$lib/sections/Hero.svelte';
import Features from '$lib/sections/Features.svelte';
import Faq from '$lib/sections/Faq.svelte';
import MobileApp from '$lib/sections/MobileApp.svelte';
import Seo from '$lib/components/Seo.svelte';
import { t } from '$lib/translations';
/** @type {import('./$types').PageProps} */
let { data } = $props();
const metadata = data.metadata;
</script>
<Head
title="home.seo.title"
description="home.seo.description"
keywords="home.seo.keywords"
url="home.seo.url"
/>
<Seo {...metadata} />
<Hero />
<Features />
<MobileApp />
<Faq />
<div class="home-container">
<section aria-labelledby="main-title">
<h1 id="main-title">{$t('home.main.title')}</h1>
{@html $t('home.main.description')}
</section>
<section aria-labelledby="features-title">
<h2 id="features-title">{$t('home.features.title')}</h2>
{@html $t('home.features.list')}
</section>
<section aria-labelledby="how-to-use-title">
<h2 id="how-to-use-title">{$t('home.howtouse.title')}</h2>
{@html $t('home.howtouse.list')}
</section>
<section aria-labelledby="testimonials-title">
<h2 id="testimonials-title">{$t('home.testimonials.title')}</h2>
{@html $t('home.testimonials.description')}
</section>
<section aria-labelledby="donation-title">
<h2 id="donation-title">{$t('home.donation.title')}</h2>
{@html $t('home.donation.description')}
<p>
<a href="/donate" class="button">{$t('home.donation.cta')}</a>
{$t('home.donation.cta.description')}
</p>
</section>
<!--TODO: add video preview-->
<section aria-labelledby="cta-title">
<h2 id="cta-title">{$t('home.cta.title')}</h2>
<p>
<a href="/viewer" class="button">{$t('home.cta.cta')}</a>
{@html $t('home.cta.cta.description')}
</p>
</section>
</div>
<style>
.home-container {
margin: 0 auto;
width: 70%;
}
@media (max-width: 768px) {
.home-container {
width: 100%;
}
}
</style>

12
src/routes/about/+page.js Normal file
View file

@ -0,0 +1,12 @@
/** @type {import('./$types').PageLoad} */
export function load() {
return {
metadata: {
title: 'about.seo.title',
description: 'about.seo.description',
keywords: 'about.seo.keywords',
url: 'about.seo.url',
image: 'about.seo.image',
},
};
}

View file

@ -1,239 +1,36 @@
<script>
import { resolve } from '$app/paths';
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
import { t } from '$lib/translations';
import { isMobile } from '$lib/utils/isMobile';
import Head from '$lib/components/Head.svelte';
import DonateIcon from '$lib/components/icons/DonateIcon.svelte';
import Seo from '$lib/components/Seo.svelte';
const backgroundImage = isMobile()
? `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/route-wallpaper-mobile.webp`
: `${PUBLIC_IMAGE_BASE_URL}/t/f_webp,w_1920,h_1080/embroidery-viewer/route-wallpaper.webp`;
/** @type {import('./$types').PageProps} */
let { data } = $props();
const metadata = data.metadata;
</script>
<Head
title="about.seo.title"
description="about.seo.description"
keywords="about.seo.keywords"
url="about.seo.url"
/>
<Seo {...metadata} />
<section aria-labelledby="about-heading">
<div
class="heading-container"
style={`background: url(${backgroundImage}) center/cover no-repeat`}
>
<div class="overlay">
<p>{$t('about.hero.tagline')}</p>
<h1 id="about-heading">{$t('about.hero.title')}</h1>
</div>
</div>
</section>
<h1 id="about-heading">{$t('about.title')}</h1>
<section id="about-content">
<div class="embroidery-bg"></div>
<h1>{$t('about.story.title')}</h1>
<div class="content-split">
<div class="split-left">
<p>{$t('about.story.p1')}</p>
<p>{$t('about.story.p2')}</p>
<p style="font-size: 1.7rem;">
{$t('about.story.quote1')}
</p>
<p>{$t('about.story.p3')}</p>
<p>{$t('about.story.p4')}</p>
<p style="font-size: 1.7rem;">
{$t('about.story.quote2')}
</p>
</div>
<div class="split-right">
<img
src={`${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/woman-sad.webp`}
style="border: 3px solid white;"
width={isMobile() ? 300 : 400}
alt=""
/>
</div>
</div>
<div
class="content-split"
style={isMobile() ? 'flex-direction: column-reverse' : ''}
>
<div class="split-left" style={isMobile() ? 'align-items: center' : ''}>
<img
src={`${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/viewer-screenshot.webp`}
style="border: 3px solid black;"
width={isMobile() ? 300 : 400}
alt=""
/>
</div>
<div class="split-right" style="align-items: flex-start;">
<p>{$t('about.product.p1')}</p>
<p>{$t('about.product.p2')}</p>
<p>{$t('about.product.p3')}</p>
<p>{$t('about.product.p4')}</p>
<p style="font-size: 1.7rem;">
{$t('about.product.quote')}
</p>
</div>
</div>
<div class="content-split">
<div class="split-left">
<h2>{$t('about.support.title')}</h2>
<p>{$t('about.support.p1')}</p>
<p>{$t('about.support.p2')}</p>
<p>{$t('about.support.p3')}</p>
</div>
<div
class="split-right"
style="align-items: center; justify-content: center;"
>
<DonateIcon color="#ffffff" size={360} />
</div>
</div>
<div class="button-container">
<a class="organic-btn-secondary" href={resolve('/support-us')}>
{$t('about.support.cta')}
</a>
</div>
{@html $t('about.content')}
</section>
<style>
.embroidery-bg {
position: absolute;
inset: 0;
z-index: 0;
pointer-events: none;
opacity: 0.15;
background-image: url("data:image/svg+xml,%3Csvg width='600' height='600' viewBox='0 0 600 600' xmlns='http://www.w3.org/2000/svg'%3E%3Cg stroke='%23ffffff' stroke-width='1.5' fill='none'%3E%3C!-- curved thread lines --%3E%3Cpath d='M50 100 Q150 50 250 120 T450 100' stroke-dasharray='6 6'/%3E%3Cpath d='M100 300 Q200 250 300 320 T500 300' stroke-dasharray='4 8'/%3E%3C!-- small crosses (stitches) --%3E%3Cg stroke-width='2'%3E%3Cpath d='M100 200 l10 10 M110 200 l-10 10'/%3E%3Cpath d='M300 400 l10 10 M310 400 l-10 10'/%3E%3Cpath d='M500 150 l10 10 M510 150 l-10 10'/%3E%3C/g%3E%3C!-- circular embroidery hoop hint --%3E%3Ccircle cx='500' cy='500' r='80' stroke-dasharray='5 10'/%3E%3C/g%3E%3C/svg%3E");
background-size: 600px 600px;
}
.button-container {
width: 100%;
display: flex;
justify-content: center;
margin-top: 70px;
}
.organic-btn-secondary {
font-size: 1.3rem;
padding: 20px 100px;
}
h2 {
color: white;
margin: 0;
text-align: center;
font-size: 2.3rem;
margin-top: 100px;
line-height: 1.5;
}
.content-split {
width: 85%;
display: flex;
section {
width: 70%;
margin: 0 auto;
padding-top: 150px;
}
.split-left {
flex: 1;
display: flex;
flex-direction: column;
}
.split-right {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.split-right {
display: flex;
flex-direction: column;
align-items: center;
flex: 1;
}
.heading-container {
position: relative;
z-index: 0;
width: 100%;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.overlay {
display: flex;
flex-direction: column;
align-items: center;
width: 80%;
z-index: 1;
text-align: center;
margin-bottom: 100px;
}
.heading-container h1 {
font-size: clamp(3.5rem, 4vw, 3.5rem);
font-weight: 700;
margin-bottom: 1rem;
line-height: 1.2;
margin-top: 0;
}
#about-content {
position: relative; /* IMPORTANT */
margin: 0;
background-color: var(--color-primary);
color: white;
padding: 100px;
overflow: hidden;
}
#about-content > *:not(.embroidery-bg) {
position: relative;
z-index: 1;
}
#about-content h1 {
color: white;
margin: 0;
text-align: center;
font-size: 2.3rem;
}
#about-content p {
font-size: 1.2rem;
line-height: 1.6;
h1 {
padding: 0;
margin-bottom: 7px;
}
@media (max-width: 768px) {
.heading-container {
section {
width: 100%;
}
.heading-container h1 {
font-size: clamp(2.8rem, 4vw, 3.5rem);
}
.content-split {
width: 100%;
flex-direction: column;
padding-top: 50px;
}
#about-content {
padding: 30px 20px;
line-height: 1.5;
}
}
</style>

View file

@ -0,0 +1,12 @@
/** @type {import('./$types').PageLoad} */
export function load() {
return {
metadata: {
title: 'donate.seo.title',
description: 'donate.seo.description',
keywords: 'donate.seo.keywords',
url: 'donate.seo.url',
image: 'donate.seo.image',
},
};
}

View file

@ -0,0 +1,186 @@
<script>
import { t } from '$lib/translations';
import bitcoin from '$lib/assets/bitcoin.svg';
import monero from '$lib/assets/monero.svg';
import paypal from '$lib/assets/paypal.svg';
import Seo from '$lib/components/Seo.svelte';
/** @type {import('./$types').PageProps} */
let { data } = $props();
const metadata = data.metadata;
const BTC_ADDRESS = 'bc1qpc4lpyr6stxrrg3u0k4clp4crlt6z4j6q845rq';
const XMR_ADDRESS =
'8A9iyTskiBh6f6GDUwnUJaYhAW13gNjDYaZYJBftX434D3XLrcGBko4a8kC4pLSfiuJAoSJ7e8rwP8W4StsVypftCp6FGwm';
let copyStatus = {
btc: '',
xmr: '',
};
/**
* @param {string} text
* @param {'btc' | 'xmr'} key
*/
async function copyToClipboard(text, key) {
try {
await navigator.clipboard.writeText(text);
copyStatus[key] = 'donate.copied';
} catch (err) {
console.error('Copy failed:', err);
copyStatus[key] = 'donate.copy.failed';
}
setTimeout(() => (copyStatus[key] = ''), 2000);
}
</script>
<Seo {...metadata} />
<section aria-labelledby="donate-title" class="donate-container">
<header>
<h1 id="donate-title">{$t('donate.title')}</h1>
<p class="donate-subtitle">{$t('donate.subtitle')}</p>
<p>{@html $t('donate.description')}</p>
</header>
<h2 id="ways-title">{$t('donate.ways')}</h2>
<div class="donation-options" aria-labelledby="ways-title">
<article class="donation-method" aria-labelledby="btc-label">
<img src={bitcoin} alt="Bitcoin QR code" width="200" height="200" />
<h3 id="btc-label">Bitcoin</h3>
<p>{$t('donate.bitcoin.description')}</p>
<button
aria-label="Copy Bitcoin address"
on:click={() => copyToClipboard(BTC_ADDRESS, 'btc')}
>
{#if copyStatus.btc}
{$t(copyStatus.btc)}
{:else}
{$t('donate.copy')}
{/if}
</button>
</article>
<article class="donation-method" aria-labelledby="xmr-label">
<img src={monero} alt="Monero QR code" width="200" height="200" />
<h3 id="xmr-label">Monero</h3>
<p>{$t('donate.monero.description')}</p>
<button
aria-label="Copy Monero address"
on:click={() => copyToClipboard(XMR_ADDRESS, 'xmr')}
>
{#if copyStatus.xmr}
{$t(copyStatus.xmr)}
{:else}
{$t('donate.copy')}
{/if}
</button>
</article>
<article class="donation-method" aria-labelledby="paypal-label">
<img src={paypal} alt="PayPal" width="200" height="200" />
<h3 id="paypal-label">PayPal</h3>
<p>{$t('donate.paypal.description')}</p>
<a
class="donation-link"
href="https://www.paypal.com/donate/?business=leo@leomurca.xyz&currency_code=USD"
target="_blank"
rel="noopener noreferrer"
aria-label="PayPal donation link"
>
{$t('donate.paypal.link')}
</a>
</article>
</div>
</section>
<style>
.donate-container {
width: 70%;
margin: 0 auto;
}
h1 {
margin-bottom: 7px;
}
.donate-subtitle {
font-weight: bold;
color: #06345f;
margin: 0;
}
.donation-options {
display: flex;
gap: 2rem;
margin-top: 2rem;
}
.donation-method {
display: flex;
flex-direction: column;
align-items: center;
width: 30rem;
}
.donation-method p {
margin-top: 0.5rem;
text-align: center;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
button,
.donation-link {
font-size: 14px;
background-color: #05345f;
font-weight: bold;
color: white;
padding: 10px;
border: none;
border-radius: 10px;
width: 200px;
height: 45px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
margin-top: 1rem;
transition: background-color 0.2s ease;
}
button:hover,
.donation-link:hover {
cursor: pointer;
background-color: black;
color: white;
}
@media (max-width: 768px) {
.donate-container {
width: 100%;
}
.donation-options {
flex-direction: column;
align-items: center;
}
.donation-method {
width: 100%;
}
button,
.donation-link {
width: 100%;
height: 55px;
font-size: 1em;
}
}
</style>

View file

@ -1,35 +0,0 @@
<script>
import { t } from '$lib/translations';
import Head from '$lib/components/Head.svelte';
</script>
<Head
title="mobile.app.privacy.policy.seo.title"
description="mobile.app.privacy.policy.seo.description"
keywords="mobile.app.privacy.policy.seo.keywords"
url="mobile.app.privacy.policy.seo.url"
/>
<section aria-labelledby="privacy-policy-heading">
<h1 id="privacy-policy-heading">{$t('mobile.app.privacy.policy.title')}</h1>
<p><em>{$t('mobile.app.privacy.policy.last.update')}</em></p>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html $t('mobile.app.privacy.policy.content')}
</section>
<style>
section {
width: 70%;
margin: 0 auto;
}
h2 {
font-size: 17px;
}
@media (max-width: 768px) {
section {
width: 90%;
}
}
</style>

View file

@ -0,0 +1,12 @@
/** @type {import('./$types').PageLoad} */
export function load() {
return {
metadata: {
title: 'privacy.policy.seo.title',
description: 'privacy.policy.seo.description',
keywords: 'privacy.policy.seo.keywords',
url: 'privacy.policy.seo.url',
image: 'privacy.policy.seo.image',
},
};
}

View file

@ -1,20 +1,19 @@
<script>
import { t } from '$lib/translations';
import Head from '$lib/components/Head.svelte';
import Seo from '$lib/components/Seo.svelte';
/** @type {import('./$types').PageProps} */
let { data } = $props();
const metadata = data.metadata;
</script>
<Head
title="privacy.policy.seo.title"
description="privacy.policy.seo.description"
keywords="privacy.policy.seo.keywords"
url="privacy.policy.seo.url"
/>
<Seo {...metadata} />
<section aria-labelledby="privacy-policy-heading">
<h1 id="privacy-policy-heading">{$t('privacy.policy.title')}</h1>
<p><em>{$t('privacy.policy.last.update')}</em></p>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html $t('privacy.policy.content')}
</section>
@ -22,7 +21,6 @@
section {
width: 70%;
margin: 0 auto;
margin-top: 130px;
}
h2 {
font-size: 17px;
@ -30,7 +28,7 @@
@media (max-width: 768px) {
section {
width: 90%;
width: 100%;
}
}
</style>

View file

@ -1,116 +0,0 @@
<script>
import { t } from '$lib/translations';
import { PUBLIC_IMAGE_BASE_URL } from '$env/static/public';
import { isMobile } from '$lib/utils/isMobile';
import BuyMeACoffeeIcon from '$lib/components/icons/BuyMeACoffeeIcon.svelte';
import Head from '$lib/components/Head.svelte';
const backgroundImage = isMobile()
? `${PUBLIC_IMAGE_BASE_URL}/t/f_webp/embroidery-viewer/route-wallpaper-mobile.webp`
: `${PUBLIC_IMAGE_BASE_URL}/t/f_webp,w_1920,h_1080/embroidery-viewer/route-wallpaper.webp`;
</script>
<Head
title="support-us.seo.title"
description="support-us.seo.description"
keywords="support-us.seo.keywords"
url="support-us.seo.url"
/>
<section aria-labelledby="support-us">
<div
class="heading-container"
style={`background: url(${backgroundImage}) center/cover no-repeat`}
>
<div class="overlay">
<h1 id="support-us">{$t('support-us.title')}</h1>
<p>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html $t('support-us.description')}
</p>
<a
class="bmac-button"
href="https://buymeacoffee.com/embroideryviewerxyz"
target="_blank"
>
<BuyMeACoffeeIcon size={30} />
{$t('support-us.cta')}
</a>
</div>
</div>
</section>
<style>
.bmac-button {
display: flex;
align-items: center;
text-decoration: none;
background-color: #ffdd03;
border: none;
padding: 15px 20px;
border-radius: 15px;
font-weight: 700;
color: black;
gap: 10px;
transition: transform 0.3s ease;
margin-top: 100px;
}
.bmac-button:hover {
transform: scale(1.1);
}
.heading-container {
position: relative;
z-index: 0;
width: 100%;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.overlay {
display: flex;
flex-direction: column;
align-items: center;
width: 80%;
z-index: 1;
text-align: center;
margin-bottom: 100px;
}
.heading-container h1 {
font-size: clamp(3.5rem, 4vw, 3.5rem);
font-weight: 700;
margin-bottom: 1rem;
line-height: 1.2;
margin-top: 0;
}
p {
font-size: 1.4rem;
line-height: 1.5;
}
strong {
font-weight: 900;
}
@media (max-width: 768px) {
.heading-container {
width: 100%;
}
.heading-container h1 {
font-size: clamp(2.5rem, 4vw, 3rem);
}
.bmac-button {
margin-top: 20px;
}
p {
font-size: 1rem;
}
}
</style>

View file

@ -0,0 +1,12 @@
/** @type {import('./$types').PageLoad} */
export function load() {
return {
metadata: {
title: 'terms.of.service.seo.title',
description: 'terms.of.service.seo.description',
keywords: 'terms.of.service.seo.keywords',
url: 'terms.of.service.seo.url',
image: 'terms.of.service.seo.image',
},
};
}

View file

@ -1,20 +1,19 @@
<script>
import { t } from '$lib/translations';
import Head from '$lib/components/Head.svelte';
import Seo from '$lib/components/Seo.svelte';
/** @type {import('./$types').PageProps} */
let { data } = $props();
const metadata = data.metadata;
</script>
<Head
title="terms.of.service.seo.title"
description="terms.of.service.seo.description"
keywords="terms.of.service.seo.keywords"
url="terms.of.service.seo.url"
/>
<Seo {...metadata} />
<section aria-labelledby="tos-heading">
<h1 id="tos-heading">{$t('terms.of.service.title')}</h1>
<p><em>{$t('terms.of.service.update')}</em></p>
<!-- eslint-disable-next-line svelte/no-at-html-tags -->
{@html $t('terms.of.service.content')}
</section>
@ -22,7 +21,6 @@
section {
width: 70%;
margin: 0 auto;
margin-top: 130px;
}
h2 {
font-size: 17px;
@ -30,7 +28,7 @@
@media (max-width: 768px) {
section {
width: 90%;
width: 100%;
}
}
</style>

View file

@ -1,12 +1,10 @@
<script>
// @ts-nocheck
import { t } from '$lib/translations';
import CardList from '$lib/components/CardList.svelte';
import Dropzone from '$lib/components/Dropzone.svelte';
import FileList from '$lib/components/FileList.svelte';
import Head from '$lib/components/Head.svelte';
import Seo from '$lib/components/Seo.svelte';
import { filterFiles } from '$lib/utils/filterFiles';
import { supportedFormats } from '$lib/format-readers';
@ -26,14 +24,7 @@
maxSize: 1000000,
};
/**
* Update the flag that indicates that the files were rendered.
*
* @param {SubmitEvent} e
*/
function onSubmit(e) {
e.preventDefault();
e.stopPropagation();
function onSubmit() {
areAcceptedFilesRendered = true;
}
@ -78,17 +69,20 @@
if (el) el.click();
}
}
/** @type {import('./$types').PageProps} */
let { data } = $props();
const metadata = data.metadata;
</script>
<Head
title="viewer.seo.title"
description="viewer.seo.description"
keywords="viewer.seo.keywords"
url="viewer.seo.url"
/>
<Seo {...metadata} />
<!-- eslint-disable svelte/no-at-html-tags -->
<form id="form" enctype="multipart/form-data" onsubmit={onSubmit}>
<form
id="form"
enctype="multipart/form-data"
on:submit|preventDefault|stopPropagation={onSubmit}
>
<div class="title-container">
<h2>{$t('viewer.title')}</h2>
</div>
@ -129,7 +123,6 @@
form {
width: fit-content;
margin: 0 auto;
margin-top: 130px;
}
.title-container {
@ -151,7 +144,7 @@
@media only screen and (max-device-width: 768px) {
#form {
width: 90%;
width: 100%;
}
}
</style>

BIN
static/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more