Create website for APK download #2

Merged
nabarun merged 4 commits from pj-mtm-vpn-website into main 2025-08-14 06:43:50 +00:00
30 changed files with 12864 additions and 0 deletions

1
.env.example Normal file
View File

@ -0,0 +1 @@
MTM_VPN_APK_URL=https://git.vdb.to/cerc-io/mtm-vpn-client-public/releases/download/mtm-vpn-android-v1.8.0-mtm-0.1.0/mtmvpn-general-release-v1.8.0-mtm-0.1.1.apk

3
.eslintrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals"]
}

View File

@ -0,0 +1,33 @@
name: Build the Webapp
on:
release:
types: [published]
env:
CERC_REGISTRY_USER_KEY: ${{ secrets.CICD_LACONIC_USER_KEY }}
CERC_REGISTRY_BOND_ID: ${{ secrets.CICD_LACONIC_BOND_ID }}
jobs:
build_webapp:
runs-on: ubuntu-latest
steps:
- name: "Clone project repository"
uses: actions/checkout@v3
- name: Use Node.js
uses: actions/setup-node@v3
with:
node-version: 20
- name: "Install Yarn"
run: npm install -g yarn
- name: "Install registry CLI"
run: |
npm config set @cerc-io:registry https://git.vdb.to/api/packages/cerc-io/npm/
yarn global add @cerc-io/laconic-registry-cli
- name: "Install jq"
run: apt -y update && apt -y install jq
- name: "Install dependencies"
run: yarn install
- name: "Lint code"
run: npm run lint
- name: "Build application"
run: npm run build

48
.gitignore vendored Normal file
View File

@ -0,0 +1,48 @@
# Dependencies
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# Build outputs
.next/
out/
build/
dist/
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Logs
logs
*.log
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IDE files
.vscode/
.idea/
*.swp
*.swo
*~
# Temporary files
*.tmp
*.temp

44
CLAUDE.md Normal file
View File

@ -0,0 +1,44 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Commands
- `npm run dev` - Start development server
- `npm run build` - Build the application for production
- `npm start` - Start production server
## Architecture Overview
This is a Next.js Progressive Web App (PWA) built with TypeScript that demonstrates PWA capabilities using the `next-pwa` plugin powered by Workbox.
### Key Technologies
- **Next.js** - React framework with SSR/SSG capabilities
- **next-pwa** - PWA integration with Workbox service worker
- **TypeScript** - Type safety throughout the application
### Project Structure
- `pages/` - Next.js pages using file-based routing
- `_app.tsx` - Global app component with PWA meta tags and manifest
- `index.tsx` - Main landing page displaying environment variables
- `api/` - API routes
- `public/` - Static assets including PWA icons and manifest
- `styles/` - CSS modules and global styles
- `scripts/` - Deployment and publishing scripts
### Environment Configuration
The app reads three environment variables that are displayed on the main page:
- `MTM_VPN_APK_URL`
These are configured in `next.config.js` and exposed to the client-side.
### PWA Configuration
- Service worker configured via `next-pwa` with destination set to `public/`
- Manifest file at `public/manifest.json`
- Icon set includes various sizes (16x16 to 512x512) in `public/icons/`
- PWA meta tags configured in `_app.tsx`
### Deployment
The repository includes scripts for deployment:
- `scripts/publish-app-record.sh` - Publishes app record
- `scripts/request-app-deployment.sh` - Requests deployment

View File

@ -0,0 +1,77 @@
import Image from 'next/image';
import styles from '../styles/LandingPage.module.css'
const LandingPage = () => {
const handleDownload = () => {
const downloadUrl = process.env.MTM_VPN_APK_URL
if (!downloadUrl) {
console.error('MTM_VPN_APK_URL environment variable not set')
return
}
const link = document.createElement('a')
link.href = downloadUrl
link.download = 'mtm-vpn.apk'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
return (
<div className={styles.container}>
<header className={styles.header}>
<nav className={styles.nav}>
<div className={styles.navContent}>
<div>
<span className={styles.logo}>MTM VPN</span>
</div>
</div>
</nav>
</header>
<main className={styles.main}>
<div className={styles.content}>
<div className={styles.textSection}>
<h1 className={styles.title}>
<span className={styles.titleWhite}>Secure VPN Service</span>
<br />
<span className={styles.titleGradient}>Pay with MTM Tokens</span>
</h1>
<button
onClick={handleDownload}
className={styles.downloadButton}
>
<svg className={styles.phoneIcon} viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" d="M10.5 1.5H8.25A2.25 2.25 0 0 0 6 3.75v16.5a2.25 2.25 0 0 0 2.25 2.25h7.5A2.25 2.25 0 0 0 18 20.25V3.75a2.25 2.25 0 0 0-2.25-2.25H13.5m-3 0V3h3V1.5m-3 0h3m-3 18.75h3" />
</svg>
Download for Android
</button>
</div>
<div className={styles.imageSection}>
<div className={styles.imageContainer}>
<Image
src="/mtm_vpn.png"
alt="MTM VPN App Screenshot"
className={styles.appImage}
width={300}
height={600}
/>
<div className={styles.imageEffect1}></div>
<div className={styles.imageEffect2}></div>
</div>
</div>
</div>
</main>
<div className={styles.backgroundEffects}>
<div className={styles.bgEffect1}></div>
<div className={styles.bgEffect2}></div>
</div>
</div>
)
}
export default LandingPage

View File

@ -0,0 +1,10 @@
# ENV for registry operations
# Bond to use
REGISTRY_BOND_ID=5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2
# Target deployer LRN
DEPLOYER_LRN=lrn://vaasl-provider/deployers/webapp-deployer-api.apps.vaasl.io
# Authority to deploy the app under
AUTHORITY=laconic-deploy

40
deploy/Dockerfile Normal file
View File

@ -0,0 +1,40 @@
ARG VARIANT=20-bullseye
FROM node:${VARIANT}
ARG USERNAME=node
ARG NPM_GLOBAL=/usr/local/share/npm-global
# Add NPM global to PATH.
ENV PATH=${NPM_GLOBAL}/bin:${PATH}
RUN \
# Configure global npm install location, use group to adapt to UID/GID changes
if ! cat /etc/group | grep -e "^npm:" > /dev/null 2>&1; then groupadd -r npm; fi \
&& usermod -a -G npm ${USERNAME} \
&& umask 0002 \
&& mkdir -p ${NPM_GLOBAL} \
&& touch /usr/local/etc/npmrc \
&& chown ${USERNAME}:npm ${NPM_GLOBAL} /usr/local/etc/npmrc \
&& chmod g+s ${NPM_GLOBAL} \
&& npm config -g set prefix ${NPM_GLOBAL} \
&& su ${USERNAME} -c "npm config -g set prefix ${NPM_GLOBAL}" \
# Install eslint
&& su ${USERNAME} -c "umask 0002 && npm install -g eslint" \
&& npm cache clean --force > /dev/null 2>&1
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
&& apt-get -y install --no-install-recommends jq bash
# laconic-so
RUN curl -LO https://git.vdb.to/cerc-io/stack-orchestrator/releases/download/latest/laconic-so && \
chmod +x ./laconic-so && \
mv ./laconic-so /usr/bin/laconic-so
# Configure the npm registry
RUN npm config set @cerc-io:registry https://git.vdb.to/api/packages/cerc-io/npm/
# DEBUG, remove
RUN yarn info @cerc-io/laconic-registry-cli
# Globally install the cli package
RUN yarn global add @cerc-io/laconic-registry-cli

83
deploy/README.md Normal file
View File

@ -0,0 +1,83 @@
# Deploy
## Setup
- Clone the repo:
```bash
git clone https://github.com/deep-stack/mtm-vpn-client-public.git
cd mtm-vpn-client-public/deploy
```
- Build registry CLI image:
```bash
docker build -t cerc/laconic-registry-cli .
# Builds image cerc/laconic-registry-cli:latest
```
- Configure `userKey` and `bondId` in the [registry CLI config](./config.yml):
- User key should be of the account that owns the `laconic-deploy` authority (owner account address: `laconic1kwx2jm6vscz38qlyujvq6msujmk8l3zangqahs`)
- The bond should also be owned by same user (owned bond's ID: `5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2`)
- If the authority is not available, follow [these steps to reserve a new authority](./reserve-new-authority.md)
```bash
nano config.yml
```
- Add configuration for registry operations:
```bash
cp .registry.env.example .registry.env
# Update values if required
nano .registry.env
```
- Add configuration for the app:
- Copy the example environment file:
```bash
cp ../.env.example .app.env
```
- Open .app.env and update the `MTM_VPN_APK_URL` variable if required:
NOTE: Use the download URL of APK from the latest stable release available at https://git.vdb.to/cerc-io/mtm-vpn-client-public/releases
```
nano .app.env
```
## Run
- Deploy MTM VPN App:
```bash
# In mtm-vpn-client-public/deploy dir
docker run -it \
-v ./:/app/deploy -w /app/deploy \
-e DEPLOYMENT_DNS=markto.market \
cerc/laconic-registry-cli:latest \
./deploy.sh
```
- Check deployment logs on deployer UI: <https://webapp-deployer-ui.apps.vaasl.io/>
- Visit deployed app: <https://markto.market>
### Remove deployment
- Remove deployment:
```bash
# In mtm-vpn-client-public/deploy dir
docker run -it \
-v ./:/app/deploy -w /app/deploy \
-e DEPLOYMENT_RECORD_ID=<deploment-record-id-to-be-removed> \
cerc/laconic-registry-cli:latest \
./remove-deployment.sh
```

9
deploy/config.yml Normal file
View File

@ -0,0 +1,9 @@
# Registry CLI config
services:
registry:
rpcEndpoint: 'https://laconicd-mainnet-1.laconic.com'
gqlEndpoint: 'https://laconicd-mainnet-1.laconic.com/api'
userKey:
bondId: 5d82586d156fb6671a9170d92f930a72a49a29afb45e30e16fff2100e30776e2
chainId: laconic-mainnet
gasPrice: 0.001alnt

128
deploy/deploy.sh Executable file
View File

@ -0,0 +1,128 @@
#!/bin/bash
# Fail on error
set -e
source .registry.env
echo "Using REGISTRY_BOND_ID: $REGISTRY_BOND_ID"
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
echo "Using AUTHORITY: $AUTHORITY"
# Repository URL
REPO_URL="https://github.com/deep-stack/mtm-vpn-client-public"
# Get the latest commit hash for a branch
BRANCH_NAME="main"
LATEST_HASH=$(git ls-remote $REPO_URL refs/heads/$BRANCH_NAME | awk '{print $1}')
PACKAGE_VERSION=$(curl -s $REPO_URL/raw/main/package.json | jq -r .version)
APP_NAME=mtm-vpn
echo "Repo: ${REPO_URL}"
echo "Latest hash: ${LATEST_HASH}"
echo "App version: ${PACKAGE_VERSION}"
echo "Deployment DNS: ${DEPLOYMENT_DNS}"
# Current date and time for note
CURRENT_DATE_TIME=$(date -u)
CONFIG_FILE=config.yml
# Reference: https://git.vdb.to/cerc-io/test-progressive-web-app/src/branch/main/scripts
# Get latest version from registry and increment application-record version
NEW_APPLICATION_VERSION=$(laconic -c $CONFIG_FILE registry record list --type ApplicationRecord --all --name "$APP_NAME" 2>/dev/null | jq -r -s ".[] | sort_by(.createTime) | reverse | [ .[] | select(.bondId == \"$REGISTRY_BOND_ID\") ] | .[0].attributes.version" | awk -F. -v OFS=. '{$NF += 1 ; print}')
if [ -z "$NEW_APPLICATION_VERSION" ] || [ "1" == "$NEW_APPLICATION_VERSION" ]; then
# Set application-record version if no previous records were found
NEW_APPLICATION_VERSION=0.0.1
fi
# Generate application-record.yml with incremented version
mkdir -p records
RECORD_FILE=./records/application-record.yml
cat >$RECORD_FILE <<EOF
record:
type: ApplicationRecord
version: $NEW_APPLICATION_VERSION
repository_ref: $LATEST_HASH
repository: ["$REPO_URL"]
app_type: webapp
name: $APP_NAME
app_version: $PACKAGE_VERSION
EOF
echo "Application record generated successfully: $RECORD_FILE"
# Publish ApplicationRecord
publish_response=$(laconic -c $CONFIG_FILE registry record publish --filename $RECORD_FILE)
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to publish record"
exit $rc
fi
RECORD_ID=$(echo $publish_response | jq -r '.id')
echo "ApplicationRecord published, setting names next"
echo $RECORD_ID
# Set name to record
REGISTRY_APP_LRN="lrn://$AUTHORITY/applications/$APP_NAME"
name1="$REGISTRY_APP_LRN@${PACKAGE_VERSION}"
sleep 2
laconic -c $CONFIG_FILE registry name set "$name1" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set name: $REGISTRY_APP_LRN@${PACKAGE_VERSION}"
exit $rc
fi
echo "$name1 set for ApplicationRecord"
name2="$REGISTRY_APP_LRN@${LATEST_HASH}"
sleep 2
laconic -c $CONFIG_FILE registry name set "$name2" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set hash"
exit $rc
fi
echo "$name2 set for ApplicationRecord"
name3="$REGISTRY_APP_LRN"
sleep 2
# Set name if latest release
laconic -c $CONFIG_FILE registry name set "$name3" "$RECORD_ID"
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to set release"
exit $rc
fi
echo "$name3 set for ApplicationRecord"
# Check if record found for REGISTRY_APP_LRN
query_response=$(laconic -c $CONFIG_FILE registry name resolve "$REGISTRY_APP_LRN")
rc=$?
if [ $rc -ne 0 ]; then
echo "FATAL: Failed to query name"
exit $rc
fi
APP_RECORD=$(echo $query_response | jq '.[0]')
if [ -z "$APP_RECORD" ] || [ "null" == "$APP_RECORD" ]; then
echo "No record found for $REGISTRY_APP_LRN."
exit 1
fi
echo "Name resolution successful"
sleep 2
echo "Requesting a webapp deployment for $name2, using deployer $DEPLOYER_LRN"
laconic-so request-webapp-deployment \
--laconic-config $CONFIG_FILE \
--deployer $DEPLOYER_LRN \
--app $name2 \
--env-file ./.app.env \
--dns $DEPLOYMENT_DNS \
--make-payment auto
echo "Done"

50
deploy/laconic-cli.sh Executable file
View File

@ -0,0 +1,50 @@
#!/bin/bash
# Laconic Registry CLI Docker wrapper script
# This script wraps the Docker command to run laconic registry CLI commands
# Run this script from the deploy directory
# Check if docker is available
if ! command -v docker &> /dev/null; then
echo "Error: Docker is not installed or not in PATH"
exit 1
fi
# Check if the cerc/laconic-registry-cli image exists
if ! docker image inspect cerc/laconic-registry-cli &> /dev/null; then
echo "Error: cerc/laconic-registry-cli Docker image not found"
echo "Please build the image first: docker build -t cerc/laconic-registry-cli ."
exit 1
fi
# Get current directory (should be deploy directory)
CURRENT_DIR="$(pwd)"
PROJECT_ROOT="$(dirname "$CURRENT_DIR")"
# Verify we're in the deploy directory
if [ ! -f "config.yml" ] || [ ! -f "laconic-cli.sh" ]; then
echo "Error: This script must be run from the deploy directory"
echo "Current directory: $CURRENT_DIR"
echo "Please cd to the deploy directory and run: ./laconic-cli.sh"
exit 1
fi
# Set up volume mounts
DEPLOY_MOUNT="-v $CURRENT_DIR:/app/deploy"
OUT_MOUNT=""
# Create out directory if it doesn't exist and always mount it
if [ ! -d "out" ]; then
mkdir -p "out"
fi
OUT_MOUNT="-v $CURRENT_DIR/out:/app/out"
# Run the Docker command with processed arguments
docker run --rm \
--add-host=host.docker.internal:host-gateway \
$DEPLOY_MOUNT \
$OUT_MOUNT \
-w /app/deploy \
cerc/laconic-registry-cli \
laconic registry -c config.yml \
"$@"

0
deploy/records/.gitkeep Normal file
View File

63
deploy/remove-deployment.sh Executable file
View File

@ -0,0 +1,63 @@
#!/bin/bash
set -e
if [[ -z $DEPLOYMENT_RECORD_ID ]]; then
echo "Error: please pass the deployment record ID" >&2
exit 1
fi
source .registry.env
echo "Using DEPLOYER_LRN: $DEPLOYER_LRN"
echo "Deployment record ID: $DEPLOYMENT_RECORD_ID"
# Generate application-deployment-removal-request.yml
REMOVAL_REQUEST_RECORD_FILE=./records/application-deployment-removal-request.yml
cat > $REMOVAL_REQUEST_RECORD_FILE <<EOF
record:
deployer: $DEPLOYER_LRN
deployment: $DEPLOYMENT_RECORD_ID
type: ApplicationDeploymentRemovalRequest
version: 1.0.0
EOF
CONFIG_FILE=config.yml
sleep 2
REMOVAL_REQUEST_ID=$(laconic -c $CONFIG_FILE registry record publish --filename $REMOVAL_REQUEST_RECORD_FILE | jq -r '.id')
echo "ApplicationDeploymentRemovalRequest published"
echo $REMOVAL_REQUEST_ID
# Deployment checks
RETRY_INTERVAL=30
MAX_RETRIES=20
# Check that an ApplicationDeploymentRemovalRecord is published
retry_count=0
while true; do
removal_records_response=$(laconic -c $CONFIG_FILE registry record list --type ApplicationDeploymentRemovalRecord --all request $REMOVAL_REQUEST_ID)
len_removal_records=$(echo $removal_records_response | jq 'length')
# Check if number of records returned is 0
if [ $len_removal_records -eq 0 ]; then
# Check if retries are exhausted
if [ $retry_count -eq $MAX_RETRIES ]; then
echo "Retries exhausted"
echo "ApplicationDeploymentRemovalRecord for deployment removal request $REMOVAL_REQUEST_ID not found"
exit 1
else
echo "ApplicationDeploymentRemovalRecord not found, retrying in $RETRY_INTERVAL sec..."
sleep $RETRY_INTERVAL
retry_count=$((retry_count+1))
fi
else
echo "ApplicationDeploymentRemovalRecord found"
REMOVAL_RECORD_ID=$(echo $removal_records_response | jq -r '.[0].id')
echo $REMOVAL_RECORD_ID
break
fi
done
echo "Deployment removal successful"

View File

@ -0,0 +1,50 @@
# Reserve a New Authority
The following steps are used to reserve `laconic-deploy` authority
- Reserve authority:
```bash
./laconic-cli.sh authority reserve laconic-deploy
```
- After reserving authority, commit phase begins which lasts for 1 minute so please commit the bid following below steps within that time period
- Check authority info:
```bash
./laconic-cli.sh authority whois laconic-deploy
```
- Note down auction ID from authority info as it is required in next steps
- Get auction info:
```bash
./laconic-cli.sh auction get <auction-id>
```
- Commit an auction bid:
```bash
# 5000000 alnt is the minimum bid amount for authority auction
./laconic-cli.sh auction bid commit <auction-id> 5000000 alnt
# Example file path inside container
Reveal file: /app/deploy/out/bafyreiay2rccax64yn4ljhvzvm3jkbebvzheyucuma5jlbpzpzd5i5gjuy.json
```
- The reveal phase starts as soon as commit phase ends and lasts for 1 minute so please reveal the bid within this time period
- Reveal bid:
```bash
./laconic-cli.sh auction bid reveal <auction-id> /app/deploy/out/<reaveal-file>.json
```
- Set authority bond after winning auction as it is required to use the published authority:
```bash
./laconic-cli.sh authority bond set laconic-deploy <bond-id>
```

5
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information.

10
next.config.js Normal file
View File

@ -0,0 +1,10 @@
/** @type {import('next').NextConfig} */
const withPWA = require('next-pwa')({
dest: 'public',
})
module.exports = withPWA({
env: {
MTM_VPN_APK_URL: process.env.MTM_VPN_APK_URL,
},
})

9341
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"private": true,
"name": "mtm-vpn-client-public",
"version": "1.8.0-mtm-0.1.1",
"repository": "https://github.com/deep-stack/mtm-vpn-client-public",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"next": "latest",
"next-pwa": "^5.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0"
},
"devDependencies": {
"@types/node": "17.0.4",
"@types/react": "17.0.38",
"eslint": "^9.33.0",
"eslint-config-next": "^15.4.6",
"typescript": "^5.9.2"
}
}

31
pages/_app.tsx Normal file
View File

@ -0,0 +1,31 @@
import Head from 'next/head'
import '../styles/globals.css'
import { AppProps } from 'next/app'
export default function MyApp({ Component, pageProps }: AppProps) {
return (
<>
<Head>
<meta charSet="utf-8" />
<meta httpEquiv="X-UA-Compatible" content="IE=edge" />
<meta
name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no"
/>
<meta name="description" content="Description" />
<meta name="keywords" content="Keywords" />
<title>MTM VPN</title>
<link rel="manifest" href="/manifest.json" />
<link
href="/mtm-vpn-logo.png"
rel="icon"
type="image/png"
/>
<link rel="apple-touch-icon" href="/mtm-vpn-logo.png"></link>
<meta name="theme-color" content="#317EFB" />
</Head>
<Component {...pageProps} />
</>
)
}

5
pages/index.tsx Normal file
View File

@ -0,0 +1,5 @@
import LandingPage from '../components/LandingPage'
export default function Home() {
return <LandingPage />
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

19
public/manifest.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "MTM VPN",
"short_name": "MTM VPN",
"theme_color": "#54C3D4",
"background_color": "#1e293b",
"display": "fullscreen",
"orientation": "portrait",
"scope": "/",
"start_url": "/",
"icons": [
{
"src": "mtm-vpn-logo.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
}
],
"splash_pages": null
}

BIN
public/mtm-vpn-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

BIN
public/mtm_vpn.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

100
public/sw.js Normal file
View File

@ -0,0 +1,100 @@
/**
* Copyright 2018 Google Inc. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// If the loader is already loaded, just stop.
if (!self.define) {
let registry = {};
// Used for `eval` and `importScripts` where we can't get script URL by other means.
// In both cases, it's safe to use a global var because those functions are synchronous.
let nextDefineUri;
const singleRequire = (uri, parentUri) => {
uri = new URL(uri + ".js", parentUri).href;
return registry[uri] || (
new Promise(resolve => {
if ("document" in self) {
const script = document.createElement("script");
script.src = uri;
script.onload = resolve;
document.head.appendChild(script);
} else {
nextDefineUri = uri;
importScripts(uri);
resolve();
}
})
.then(() => {
let promise = registry[uri];
if (!promise) {
throw new Error(`Module ${uri} didnt register its module`);
}
return promise;
})
);
};
self.define = (depsNames, factory) => {
const uri = nextDefineUri || ("document" in self ? document.currentScript.src : "") || location.href;
if (registry[uri]) {
// Module is already loading or loaded.
return;
}
let exports = {};
const require = depUri => singleRequire(depUri, uri);
const specialDeps = {
module: { uri },
exports,
require
};
registry[uri] = Promise.all(depsNames.map(
depName => specialDeps[depName] || require(depName)
)).then(deps => {
factory(...deps);
return exports;
});
};
}
define(['./workbox-e43f5367'], (function (workbox) { 'use strict';
importScripts();
self.skipWaiting();
workbox.clientsClaim();
workbox.registerRoute("/", new workbox.NetworkFirst({
"cacheName": "start-url",
plugins: [{
cacheWillUpdate: async ({
request,
response,
event,
state
}) => {
if (response && response.type === 'opaqueredirect') {
return new Response(response.body, {
status: 200,
statusText: 'OK',
headers: response.headers
});
}
return response;
}
}]
}), 'GET');
workbox.registerRoute(/.*/i, new workbox.NetworkOnly({
"cacheName": "dev",
plugins: []
}), 'GET');
}));

2455
public/workbox-e43f5367.js Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,198 @@
.container {
min-height: 100vh;
background: linear-gradient(135deg, #1e293b 0%, #1e40af 50%, #54c3d4 100%);
position: relative;
overflow: hidden;
}
.backgroundEffects {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
pointer-events: none;
z-index: 1;
}
.bgEffect1 {
position: absolute;
top: -160px;
right: -160px;
width: 320px;
height: 320px;
background: rgba(84, 195, 212, 0.1);
border-radius: 50%;
filter: blur(60px);
}
.bgEffect2 {
position: absolute;
bottom: -160px;
left: -160px;
width: 320px;
height: 320px;
background: rgba(34, 211, 238, 0.1);
border-radius: 50%;
filter: blur(60px);
}
.header {
position: relative;
z-index: 10;
}
.nav {
max-width: 1200px;
margin: 0 auto;
padding: 0 1rem;
}
.navContent {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem 0;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
color: white;
}
.main {
position: relative;
z-index: 10;
max-width: 1200px;
margin: 0 auto;
padding: 3rem 1rem;
}
.content {
display: grid;
grid-template-columns: 1fr;
gap: 3rem;
align-items: center;
}
@media (min-width: 1024px) {
.content {
grid-template-columns: 1fr 1fr;
}
}
.textSection {
text-align: center;
}
@media (min-width: 1024px) {
.textSection {
text-align: left;
}
}
.title {
font-size: 2.5rem;
font-weight: 800;
margin-bottom: 1.5rem;
line-height: 1.1;
}
@media (min-width: 768px) {
.title {
font-size: 3.75rem;
}
}
.titleWhite {
color: white;
}
.titleGradient {
background: linear-gradient(45deg, #54c3d4, #22d3ee);
-webkit-background-clip: text;
background-clip: text;
-webkit-text-fill-color: transparent;
color: transparent;
}
.downloadButton {
display: inline-flex;
align-items: center;
padding: 1rem 2rem;
font-size: 1.125rem;
font-weight: 600;
color: #1e293b;
background: white;
border: none;
border-radius: 9999px;
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
cursor: pointer;
transition: all 0.2s;
text-decoration: none;
}
.downloadButton:hover {
background: #f3f4f6;
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
transform: scale(1.05);
}
.phoneIcon {
width: 1.5rem;
height: 1.5rem;
margin-right: 0.5rem;
stroke: currentColor;
fill: none;
stroke-width: 2;
}
.imageSection {
display: flex;
justify-content: center;
margin-top: 2rem;
}
@media (min-width: 1024px) {
.imageSection {
justify-content: flex-end;
margin-top: 0;
}
}
.imageContainer {
position: relative;
width: 288px;
max-width: 100%;
}
.appImage {
width: 100%;
height: auto;
border-radius: 1.5rem;
box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
border: 4px solid rgba(255, 255, 255, 0.2);
}
.imageEffect1 {
position: absolute;
top: -1rem;
right: -1rem;
width: 4rem;
height: 4rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
filter: blur(12px);
}
.imageEffect2 {
position: absolute;
bottom: -1rem;
left: -1rem;
width: 5rem;
height: 5rem;
background: rgba(255, 255, 255, 0.1);
border-radius: 50%;
filter: blur(12px);
}

16
styles/globals.css Normal file
View File

@ -0,0 +1,16 @@
html,
body {
padding: 0;
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Oxygen,
Ubuntu, Cantarell, Fira Sans, Droid Sans, Helvetica Neue, sans-serif;
}
a {
color: inherit;
text-decoration: none;
}
* {
box-sizing: border-box;
}

20
tsconfig.json Normal file
View File

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"]
}