Compare commits
19
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5c1655256 | ||
|
|
3d970aab45 | ||
|
|
93df14169c | ||
|
|
7fc43d0f09 | ||
|
|
254a2f154d | ||
|
|
b74ea9307e | ||
|
|
53c45da00a | ||
|
|
136af9caf2 | ||
|
|
fbc8e0b16a | ||
|
|
aa4686254b | ||
|
|
2d80ed05db | ||
|
|
d927b95457 | ||
|
|
2dc9dd985a | ||
|
|
80a6aa9fb5 | ||
|
|
1343404e01 | ||
|
|
5faba43728 | ||
|
|
a20f5eff85 | ||
|
|
18e835450a | ||
|
|
f5ad55235c |
No files matched your search
@@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
if [[ ! -d "/Users/ms/Development/personal/imphnen-frontend-service" ]]; then
|
|
||||||
echo "Cannot find source directory; Did you move it?"
|
|
||||||
echo "(Looking for "/Users/ms/Development/personal/imphnen-frontend-service")"
|
|
||||||
echo 'Cannot force reload with this script - use "direnv reload" manually and then try again'
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# rebuild the cache forcefully
|
|
||||||
_nix_direnv_force_reload=1 direnv exec "/Users/ms/Development/personal/imphnen-frontend-service" true
|
|
||||||
|
|
||||||
# Update the mtime for .envrc.
|
|
||||||
# This will cause direnv to reload again - but without re-building.
|
|
||||||
touch "/Users/ms/Development/personal/imphnen-frontend-service/.envrc"
|
|
||||||
|
|
||||||
# Also update the timestamp of whatever profile_rc we have.
|
|
||||||
# This makes sure that we know we are up to date.
|
|
||||||
touch -r "/Users/ms/Development/personal/imphnen-frontend-service/.envrc" "/Users/ms/Development/personal/imphnen-frontend-service/.direnv"/*.rc
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/nix/store/01x5k4nlxcpyd85nnr0b9gm89rm8ff4x-source
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/nix/store/8hx8jwa24q6rkzhca9iqy5ckk9rbphi1-source
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/nix/store/jizz7ld5r2w11y03wrmnvwsskc45krj9-source
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/nix/store/yj1wxm9hh8610iyzqnz75kvs6xl8j3my-source
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
/nix/store/y4p9szcab9xd7v1c7k9ghr86hb6myyg4-nix-shell-env
|
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
VITE_SUPABASE_URL=https://your-project-id.supabase.co
|
||||||
|
VITE_SUPABASE_ANON_KEY=your-anon-key-here
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Deploy Backoffice
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/backoffice/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-vps
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
command_timeout: 30m
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-backoffice ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-backoffice
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-backoffice && git fetch origin && git reset --hard origin/develop && git pull
|
||||||
|
fi
|
||||||
|
cat > ~/imphnen-frontend-service-backoffice/apps/backoffice/.env << EOF
|
||||||
|
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
||||||
|
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
||||||
|
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
EOF
|
||||||
|
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml down || true
|
||||||
|
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml build --progress=plain
|
||||||
|
docker compose -f ~/imphnen-frontend-service-backoffice/docker-compose-backoffice.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Deploy Dimentorin
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/dimentorin/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-vps
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
command_timeout: 30m
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-dimentorin ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-dimentorin
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-dimentorin && git fetch origin && git reset --hard origin/develop && git pull
|
||||||
|
fi
|
||||||
|
cat > ~/imphnen-frontend-service-dimentorin/apps/dimentorin/.env << EOF
|
||||||
|
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
||||||
|
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
||||||
|
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
EOF
|
||||||
|
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml down || true
|
||||||
|
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml build --progress=plain
|
||||||
|
docker compose -f ~/imphnen-frontend-service-dimentorin/docker-compose-dimentorin.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Deploy Gacha
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/gacha/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-vps
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
command_timeout: 30m
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-gacha ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-gacha
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-gacha && git fetch origin && git reset --hard origin/develop && git pull
|
||||||
|
fi
|
||||||
|
cat > ~/imphnen-frontend-service-gacha/apps/gacha/.env << EOF
|
||||||
|
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
||||||
|
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
||||||
|
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
EOF
|
||||||
|
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml down || true
|
||||||
|
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml build --progress=plain
|
||||||
|
docker compose -f ~/imphnen-frontend-service-gacha/docker-compose-gacha.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
name: Deploy Hackathon
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/hackathon/**' # Auto deploy when changes pushed to apps/hackathon
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-vps
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
command_timeout: 30m
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service-hackathon ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service-hackathon
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service-hackathon && git fetch origin && git reset --hard origin/develop && git pull
|
||||||
|
fi
|
||||||
|
cat > ~/imphnen-frontend-service-hackathon/.env.local << EOF
|
||||||
|
VITE_API_URL=${{ secrets.VITE_API_URL }}
|
||||||
|
VITE_SUPABASE_URL=${{ secrets.VITE_SUPABASE_URL }}
|
||||||
|
VITE_SUPABASE_ANON_KEY=${{ secrets.VITE_SUPABASE_ANON_KEY }}
|
||||||
|
EOF
|
||||||
|
docker compose -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml down || true
|
||||||
|
docker builder prune -af || true
|
||||||
|
DOCKER_BUILDKIT=1 docker compose --env-file ~/imphnen-frontend-service-hackathon/.env.local -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml build --progress=plain
|
||||||
|
docker compose -f ~/imphnen-frontend-service-hackathon/docker-compose-hackathon.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
name: Deploy Landing
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
paths:
|
||||||
|
- 'apps/landing/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-vps
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repo
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Deploy via SSH
|
||||||
|
uses: appleboy/ssh-action@v1.0.3
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.SSH_HOST }}
|
||||||
|
username: ${{ secrets.SSH_USER }}
|
||||||
|
key: ${{ secrets.SSH_KEY }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
command_timeout: 30m
|
||||||
|
script: |
|
||||||
|
set -e
|
||||||
|
if [ ! -d ~/imphnen-frontend-service ]; then
|
||||||
|
git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service
|
||||||
|
else
|
||||||
|
cd ~/imphnen-frontend-service && git fetch origin && git reset --hard origin/develop && git pull
|
||||||
|
fi
|
||||||
|
cat > ~/imphnen-frontend-service/apps/landing/.env << EOF
|
||||||
|
NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}
|
||||||
|
NEXT_PUBLIC_SUPABASE_URL=${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
|
||||||
|
NEXT_PUBLIC_SUPABASE_ANON_KEY=${{ secrets.NEXT_PUBLIC_SUPABASE_ANON_KEY }}
|
||||||
|
EOF
|
||||||
|
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml down || true
|
||||||
|
DOCKER_BUILDKIT=1 docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml build --progress=plain
|
||||||
|
docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml up -d
|
||||||
|
docker image prune -af
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
name: Nix Build & Cache
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ['develop']
|
|
||||||
pull_request:
|
|
||||||
branches: ['develop']
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Install Nix
|
|
||||||
uses: DeterminateSystems/nix-installer-action@main
|
|
||||||
|
|
||||||
- name: Setup Cachix
|
|
||||||
uses: cachix/cachix-action@v15
|
|
||||||
with:
|
|
||||||
name: msdqn
|
|
||||||
authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}'
|
|
||||||
|
|
||||||
- name: Build all packages
|
|
||||||
run: |
|
|
||||||
nix build .#landing -o result-landing
|
|
||||||
nix build .#backoffice -o result-backoffice
|
|
||||||
nix build .#gacha -o result-gacha
|
|
||||||
nix build .#dimentorin -o result-dimentorin
|
|
||||||
nix build .#hackathon -o result-hackathon
|
|
||||||
nix build .#infra -o result-infra
|
|
||||||
|
|
||||||
- name: Show build outputs
|
|
||||||
run: |
|
|
||||||
echo "Landing: $(readlink result-landing)"
|
|
||||||
echo "Backoffice: $(readlink result-backoffice)"
|
|
||||||
echo "Gacha: $(readlink result-gacha)"
|
|
||||||
echo "Dimentorin: $(readlink result-dimentorin)"
|
|
||||||
echo "Hackathon: $(readlink result-hackathon)"
|
|
||||||
echo "Infra: $(readlink result-infra)"
|
|
||||||
@@ -10,7 +10,6 @@ This repository is a **monorepo** for all frontend services of IMPHNEN. The mono
|
|||||||
2. **Backoffice** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Internal Management Website</a>.
|
2. **Backoffice** - Application for <a href="https://gacha.imphnen.dev/" target="_blank">Internal Management Website</a>.
|
||||||
3. **Dimentorin** - Application for <a href="https://dimentorin.imphnen.dev/" target="_blank">Mentoring Service</a>.
|
3. **Dimentorin** - Application for <a href="https://dimentorin.imphnen.dev/" target="_blank">Mentoring Service</a>.
|
||||||
4. **Landing Page** - Application for <a href="https://imphnen.dev/" target="_blank">Landing Page</a>.
|
4. **Landing Page** - Application for <a href="https://imphnen.dev/" target="_blank">Landing Page</a>.
|
||||||
5. **QR Campaign** - Application for QR Campaign Management.
|
|
||||||
|
|
||||||
## How to install
|
## How to install
|
||||||
|
|
||||||
@@ -55,10 +54,6 @@ Use the following commands to run in development mode:
|
|||||||
```sh
|
```sh
|
||||||
npm run landing:dev
|
npm run landing:dev
|
||||||
```
|
```
|
||||||
- **QR Campaign**:
|
|
||||||
```sh
|
|
||||||
npm run qrcampaign:dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build
|
### Build
|
||||||
|
|
||||||
@@ -80,10 +75,6 @@ Use the following commands to build the applications:
|
|||||||
```sh
|
```sh
|
||||||
npm run landing:build
|
npm run landing:build
|
||||||
```
|
```
|
||||||
- **QR Campaign**:
|
|
||||||
```sh
|
|
||||||
npm run qrcampaign:build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
### Production
|
||||||
|
|
||||||
@@ -105,10 +96,6 @@ Use the following commands to run the applications in production mode:
|
|||||||
```sh
|
```sh
|
||||||
npm run landing:prod
|
npm run landing:prod
|
||||||
```
|
```
|
||||||
- **QR Campaign**:
|
|
||||||
```sh
|
|
||||||
npm run qrcampaign:prod
|
|
||||||
```
|
|
||||||
|
|
||||||
### Storybook
|
### Storybook
|
||||||
|
|
||||||
|
|||||||
@@ -18,9 +18,6 @@
|
|||||||
type="image/svg+xml"
|
type="image/svg+xml"
|
||||||
href="/images/imphnen-logo-simple.svg"
|
href="/images/imphnen-logo-simple.svg"
|
||||||
/>
|
/>
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@400;600;700&display=swap" rel="stylesheet" />
|
|
||||||
<link rel="stylesheet" href="/src/index.css" />
|
<link rel="stylesheet" href="/src/index.css" />
|
||||||
<meta property="og:type" content="website" />
|
<meta property="og:type" content="website" />
|
||||||
<meta property="og:url" content="https://hackathon.imphnen.dev/" />
|
<meta property="og:url" content="https://hackathon.imphnen.dev/" />
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 263 KiB After Width: | Height: | Size: 1.0 MiB |
@@ -1,142 +0,0 @@
|
|||||||
<svg width="842" height="595" viewBox="0 0 842 595" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<g clip-path="url(#clip0_10_25)">
|
|
||||||
<rect width="595" height="842" transform="translate(842 3.68059e-05) rotate(90)" fill="white"/>
|
|
||||||
<line x1="842.5" y1="3.00002" x2="-10.4999" y2="3.00002" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="28" x2="-10.4999" y2="28" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="53" x2="-10.4999" y2="53" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="78" x2="-10.4999" y2="78" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="103" x2="-10.4999" y2="103" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="128" x2="-10.4999" y2="128" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="153" x2="-10.4999" y2="153" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="178" x2="-10.4999" y2="178" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="203" x2="-10.4999" y2="203" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="228" x2="-10.4999" y2="228" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="253" x2="-10.4999" y2="253" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="278" x2="-10.4999" y2="278" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="303" x2="-10.4999" y2="303" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="328" x2="-10.4999" y2="328" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="353" x2="-10.4999" y2="353" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="378" x2="-10.4999" y2="378" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="403" x2="-10.4999" y2="403" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="428" x2="-10.4999" y2="428" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="453" x2="-10.4999" y2="453" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="478" x2="-10.4999" y2="478" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="503" x2="-10.4999" y2="503" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="528" x2="-10.4999" y2="528" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="553" x2="-10.4999" y2="553" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="578" x2="-10.4999" y2="578" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="0.500026" y1="-0.499962" x2="0.500052" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="24.5" y1="-0.499962" x2="24.5001" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="48.5001" y1="-0.499962" x2="48.5001" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="72.5001" y1="-0.499962" x2="72.5002" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="96.5001" y1="-0.499962" x2="96.5002" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="120.5" y1="-0.499962" x2="120.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="144.5" y1="-0.499962" x2="144.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="168.5" y1="-0.499962" x2="168.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="192.5" y1="-0.499962" x2="192.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="216.5" y1="-0.499962" x2="216.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="240.5" y1="-0.499962" x2="240.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="264.5" y1="-0.499962" x2="264.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="288.5" y1="-0.499962" x2="288.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="312.5" y1="-0.499962" x2="312.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="336.5" y1="-0.499962" x2="336.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="360.501" y1="-0.499962" x2="360.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="384.501" y1="-0.499962" x2="384.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="408.501" y1="-0.499962" x2="408.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="432.501" y1="-0.499962" x2="432.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="456.501" y1="-0.499962" x2="456.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="480.501" y1="-0.499962" x2="480.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="504.501" y1="-0.499962" x2="504.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="528.501" y1="-0.499962" x2="528.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="552.501" y1="-0.499962" x2="552.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="576.501" y1="-0.499962" x2="576.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="600.501" y1="-0.499962" x2="600.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="624.501" y1="-0.499962" x2="624.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="648.501" y1="-0.499962" x2="648.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="672.501" y1="-0.499962" x2="672.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="696.501" y1="-0.499962" x2="696.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="720.501" y1="-0.499962" x2="720.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="744.501" y1="-0.499962" x2="744.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="768.501" y1="-0.499962" x2="768.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="792.501" y1="-0.499962" x2="792.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="816.501" y1="-0.499962" x2="816.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="840.501" y1="-0.499962" x2="840.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<g opacity="0.4" filter="url(#filter0_f_10_25)">
|
|
||||||
<circle cx="12.5" cy="691.5" r="159" fill="#26A5EE"/>
|
|
||||||
</g>
|
|
||||||
<g filter="url(#filter1_f_10_25)">
|
|
||||||
<circle cx="728.5" cy="-82.5" r="159" fill="#26A5EE"/>
|
|
||||||
</g>
|
|
||||||
<g filter="url(#filter2_f_10_25)">
|
|
||||||
<circle cx="80.5" cy="-22.5" r="159" fill="white"/>
|
|
||||||
</g>
|
|
||||||
<path d="M709.106 -17.9754C710.65 -32.2347 729.366 -36.9065 737.616 -25.0919L783.868 41.1472C786.461 44.8615 790.572 47.2742 795.122 47.7526L876.266 56.2838C890.739 57.8054 895.481 76.2446 883.49 84.3724L816.258 129.941C812.488 132.496 810.039 136.547 809.553 141.029L800.894 220.975C799.35 235.235 780.634 239.907 772.385 228.092L726.133 161.853C723.539 158.139 719.428 155.726 714.878 155.247L633.734 146.716C619.261 145.195 614.519 126.755 626.511 118.628L693.743 73.0589C697.513 70.5037 699.961 Line truncated
|
|
||||||
<g filter="url(#filter3_d_10_25)">
|
|
||||||
<path d="M642.31 -28.5547C652.983 -100.399 747.124 -123.897 791.232 -65.7287L793.332 -62.8632L826.041 -16.0052L883.443 -9.96234C960.336 -1.87639 985.529 96.0866 921.819 139.268L874.259 171.495L868.141 228.045C859.936 303.802 760.504 328.621 716.674 265.854L683.948 219L626.551 212.973C549.664 204.884 524.475 106.926 588.175 63.7424L635.731 31.5L641.864 -25.0539L642.31 -28.5547ZM737.608 -25.0917C729.358 -36.9023 710.649 -32.2339 709.104 -17.9766L700.455 61.9764L700.18 63.6224C699.336 67.4374 697.Line truncated
|
|
||||||
<path d="M636.279 -29.4501C647.72 -106.458 748.007 -131.585 795.533 -70.1396L796.09 -69.412L796.15 -69.332L798.249 -66.4658L798.291 -66.4101L798.331 -66.3525L829.446 -21.7773L884.081 -16.0253L885.044 -15.9189C966.691 -6.53864 993.51 98.0418 925.239 144.315L925.239 144.315L880.015 174.958L874.202 228.7L874.202 228.701C865.375 310.203 758.648 336.611 711.676 269.344L711.676 269.345L680.543 224.772L625.914 219.036L625.913 219.036C543.521 210.368 516.228 105.152 584.754 58.6963L629.974 28.0372L635.Line truncated
|
|
||||||
</g>
|
|
||||||
<rect x="30.5" y="40.5" width="236" height="53" fill="url(#pattern0_10_25)"/>
|
|
||||||
<rect x="249" y="126" width="37" height="218" rx="13.5" transform="rotate(90 249 126)" fill="url(#paint0_linear_10_25)"/>
|
|
||||||
<rect x="249" y="126" width="37" height="218" rx="13.5" transform="rotate(90 249 126)" stroke="#E1ECFC"/>
|
|
||||||
<path d="M48.7 151.64C47.7267 151.64 46.8467 151.473 46.06 151.14C45.2867 150.807 44.6734 150.327 44.22 149.7C43.7667 149.073 43.5334 148.333 43.52 147.48H46.52C46.56 148.053 46.76 148.507 47.12 148.84C47.4934 149.173 48 149.34 48.64 149.34C49.2934 149.34 49.8067 149.187 50.18 148.88C50.5534 148.56 50.74 148.147 50.74 147.64C50.74 147.227 50.6134 146.887 50.36 146.62C50.1067 146.353 49.7867 146.147 49.4 146C49.0267 145.84 48.5067 145.667 47.84 145.48C46.9334 145.213 46.1934 144.953 45.62 144.7CLine truncated
|
|
||||||
<path d="M36.54 195.6C38.02 195.6 39.3134 195.887 40.42 196.46C41.54 197.02 42.4 197.833 43 198.9C43.6134 199.953 43.92 201.187 43.92 202.6C43.92 204.013 43.6134 205.24 43 206.28C42.4 207.32 41.54 208.12 40.42 208.68C39.3134 209.227 38.02 209.5 36.54 209.5H32V195.6H36.54ZM36.54 207.64C38.1667 207.64 39.4134 207.2 40.28 206.32C41.1467 205.44 41.58 204.2 41.58 202.6C41.58 200.987 41.1467 199.727 40.28 198.82C39.4134 197.913 38.1667 197.46 36.54 197.46H34.28V207.64H36.54ZM46.9397 197.02C46.5264 19Line truncated
|
|
||||||
<path d="M38.884 363.38V364.868H35.924V374.5H34.1V364.868H31.124V363.38H38.884ZM48.4913 369.876C48.4913 370.207 48.4699 370.505 48.4273 370.772H41.6913C41.7446 371.476 42.0059 372.041 42.4753 372.468C42.9446 372.895 43.5206 373.108 44.2033 373.108C45.1846 373.108 45.8779 372.697 46.2833 371.876H48.2513C47.9846 372.687 47.4993 373.353 46.7953 373.876C46.1019 374.388 45.2379 374.644 44.2033 374.644C43.3606 374.644 42.6033 374.457 41.9313 374.084C41.2699 373.7 40.7473 373.167 40.3633 372.484C39.98Line truncated
|
|
||||||
<path d="M606.364 497.5L603.9 493.148H602.844V497.5H600.604V486.332H604.796C605.66 486.332 606.396 486.487 607.004 486.796C607.612 487.095 608.065 487.505 608.364 488.028C608.673 488.54 608.828 489.116 608.828 489.756C608.828 490.492 608.615 491.159 608.188 491.756C607.761 492.343 607.127 492.748 606.284 492.972L608.956 497.5H606.364ZM602.844 491.468H604.716C605.324 491.468 605.777 491.324 606.076 491.036C606.375 490.737 606.524 490.327 606.524 489.804C606.524 489.292 606.375 488.897 606.076 48Line truncated
|
|
||||||
<path d="M599.944 511.312C599.944 510.496 600.132 509.764 600.508 509.116C600.892 508.468 601.408 507.964 602.056 507.604C602.712 507.236 603.428 507.052 604.204 507.052C605.092 507.052 605.88 507.272 606.568 507.712C607.264 508.144 607.768 508.76 608.08 509.56H606.436C606.22 509.12 605.92 508.792 605.536 508.576C605.152 508.36 604.708 508.252 604.204 508.252C603.652 508.252 603.16 508.376 602.728 508.624C602.296 508.872 601.956 509.228 601.708 509.692C601.468 510.156 601.348 510.696 601.348 51Line truncated
|
|
||||||
<path d="M400.452 495.372H396.004L395.268 497.5H392.916L396.932 486.316H399.54L403.556 497.5H401.188L400.452 495.372ZM399.844 493.58L398.228 488.908L396.612 493.58H399.844ZM406.977 485.66V497.5H404.737V485.66H406.977ZM410.006 487.58C409.612 487.58 409.281 487.457 409.014 487.212C408.758 486.956 408.63 486.641 408.63 486.268C408.63 485.895 408.758 485.585 409.014 485.34C409.281 485.084 409.612 484.956 410.006 484.956C410.401 484.956 410.726 485.084 410.982 485.34C411.249 485.585 411.382 485.895 Line truncated
|
|
||||||
<path d="M392.944 511.312C392.944 510.496 393.132 509.764 393.508 509.116C393.892 508.468 394.408 507.964 395.056 507.604C395.712 507.236 396.428 507.052 397.204 507.052C398.092 507.052 398.88 507.272 399.568 507.712C400.264 508.144 400.768 508.76 401.08 509.56H399.436C399.22 509.12 398.92 508.792 398.536 508.576C398.152 508.36 397.708 508.252 397.204 508.252C396.652 508.252 396.16 508.376 395.728 508.624C395.296 508.872 394.956 509.228 394.708 509.692C394.468 510.156 394.348 510.696 394.348 51Line truncated
|
|
||||||
<path d="M284.868 486.332V497.5H282.628V492.748H277.844V497.5H275.604V486.332H277.844V490.924H282.628V486.332H284.868ZM286.177 493.036C286.177 492.14 286.353 491.345 286.705 490.652C287.067 489.959 287.553 489.425 288.161 489.052C288.779 488.679 289.467 488.492 290.225 488.492C290.886 488.492 291.462 488.625 291.953 488.892C292.454 489.159 292.854 489.495 293.153 489.9V488.636H295.409V497.5H293.153V496.204C292.865 496.62 292.465 496.967 291.953 497.244C291.451 497.511 290.87 497.644 290.209 497Line truncated
|
|
||||||
<path d="M280.428 513.796H276.936L276.336 515.5H274.908L277.896 507.148H279.48L282.468 515.5H281.028L280.428 513.796ZM280.044 512.68L278.688 508.804L277.32 512.68H280.044ZM283.071 512.164C283.071 511.5 283.207 510.912 283.479 510.4C283.759 509.888 284.135 509.492 284.607 509.212C285.087 508.924 285.619 508.78 286.203 508.78C286.635 508.78 287.059 508.876 287.475 509.068C287.899 509.252 288.235 509.5 288.483 509.812V506.62H289.863V515.5H288.483V514.504C288.259 514.824 287.947 515.088 287.547 515Line truncated
|
|
||||||
<path d="M43.78 486.332V497.5H41.54V490.236L38.548 497.5H36.852L33.844 490.236V497.5H31.604V486.332H34.148L37.7 494.636L41.252 486.332H43.78ZM54.0427 488.636V497.5H51.7867V496.38C51.4987 496.764 51.12 497.068 50.6507 497.292C50.192 497.505 49.6907 497.612 49.1467 497.612C48.4533 497.612 47.84 497.468 47.3067 497.18C46.7733 496.881 46.352 496.449 46.0427 495.884C45.744 495.308 45.5947 494.625 45.5947 493.836V488.636H47.8347V493.516C47.8347 494.22 48.0107 494.764 48.3627 495.148C48.7147 495.521 4Line truncated
|
|
||||||
<path d="M36.428 513.796H32.936L32.336 515.5H30.908L33.896 507.148H35.48L38.468 515.5H37.028L36.428 513.796ZM36.044 512.68L34.688 508.804L33.32 512.68H36.044ZM39.0712 512.164C39.0712 511.5 39.2072 510.912 39.4792 510.4C39.7592 509.888 40.1352 509.492 40.6072 509.212C41.0872 508.924 41.6192 508.78 42.2032 508.78C42.6352 508.78 43.0592 508.876 43.4752 509.068C43.8992 509.252 44.2352 509.5 44.4832 509.812V506.62H45.8632V515.5H44.4832V514.504C44.2592 514.824 43.9472 515.088 43.5472 515.296C43.1552 Line truncated
|
|
||||||
<path d="M257.216 540.004C258.032 540.004 258.756 540.188 259.388 540.556C260.028 540.916 260.524 541.424 260.876 542.08C261.236 542.728 261.416 543.468 261.416 544.3C261.416 545.132 261.236 545.876 260.876 546.532C260.524 547.18 260.028 547.688 259.388 548.056C258.756 548.416 258.032 548.596 257.216 548.596C256.392 548.596 255.66 548.416 255.02 548.056C254.388 547.688 253.892 547.18 253.532 546.532C253.18 545.876 253.004 545.132 253.004 544.3C253.004 543.468 253.18 542.728 253.532 542.08C253.8Line truncated
|
|
||||||
<path d="M35.216 540.004C36.032 540.004 36.756 540.188 37.388 540.556C38.028 540.916 38.524 541.424 38.876 542.08C39.236 542.728 39.416 543.468 39.416 544.3C39.416 545.132 39.236 545.876 38.876 546.532C38.524 547.18 38.028 547.688 37.388 548.056C36.756 548.416 36.032 548.596 35.216 548.596C34.392 548.596 33.66 548.416 33.02 548.056C32.388 547.688 31.892 547.18 31.532 546.532C31.18 545.876 31.004 545.132 31.004 544.3C31.004 543.468 31.18 542.728 31.532 542.08C31.892 541.424 32.388 540.916 33.02 Line truncated
|
|
||||||
<rect x="782" y="176.5" width="200" height="200" rx="14.2857" transform="rotate(90 782 176.5)" fill="white"/>
|
|
||||||
<rect x="784.232" y="174.268" width="204.464" height="204.464" rx="16.5179" transform="rotate(90 784.232 174.268)" stroke="url(#paint2_linear_10_25)" stroke-opacity="0.8" stroke-width="4.46429"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<filter id="filter0_f_10_25" x="-486.5" y="192.5" width="998" height="998" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
||||||
<feGaussianBlur stdDeviation="170" result="effect1_foregroundBlur_10_25"/>
|
|
||||||
</filter>
|
|
||||||
<filter id="filter1_f_10_25" x="209.5" y="-601.5" width="1038" height="1038" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
||||||
<feGaussianBlur stdDeviation="180" result="effect1_foregroundBlur_10_25"/>
|
|
||||||
</filter>
|
|
||||||
<filter id="filter2_f_10_25" x="-257.655" y="-360.655" width="676.31" height="676.31" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
||||||
<feGaussianBlur stdDeviation="89.5775" result="effect1_foregroundBlur_10_25"/>
|
|
||||||
</filter>
|
|
||||||
<filter id="filter3_d_10_25" x="524.529" y="-110.685" width="445.702" height="439.609" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
||||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
|
||||||
<feOffset dx="-15.241" dy="15.241"/>
|
|
||||||
<feComposite in2="hardAlpha" operator="out"/>
|
|
||||||
<feColorMatrix type="matrix" values="0 0 0 0 0.152941 0 0 0 0 0.635294 0 0 0 0 0.913725 0 0 0 1 0"/>
|
|
||||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_10_25"/>
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_10_25" result="shape"/>
|
|
||||||
</filter>
|
|
||||||
<pattern id="pattern0_10_25" patternContentUnits="objectBoundingBox" width="1" height="1">
|
|
||||||
<use xlink:href="#image0_10_25" transform="matrix(0.000918274 0 0 0.00408892 0 -0.000892285)"/>
|
|
||||||
</pattern>
|
|
||||||
<linearGradient id="paint0_linear_10_25" x1="249.5" y1="235" x2="287.5" y2="235" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="white"/>
|
|
||||||
<stop offset="1" stop-color="#F9FDFF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint1_linear_10_25" x1="42.5" y1="144.5" x2="237.5" y2="144.5" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#22A8F1"/>
|
|
||||||
<stop offset="1" stop-color="#5CBFF5"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint2_linear_10_25" x1="782" y1="276.5" x2="982" y2="276.5" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#E3F4FD"/>
|
|
||||||
<stop offset="1" stop-color="#1FAAEC"/>
|
|
||||||
</linearGradient>
|
|
||||||
<clipPath id="clip0_10_25">
|
|
||||||
<rect width="595" height="842" fill="white" transform="translate(842 3.68059e-05) rotate(90)"/>
|
|
||||||
</clipPath>
|
|
||||||
<image id="image0_10_25" width="1089" height="245" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABEEAAAD1CAYAAAC/deukAAAACXBIWXMAAAsSAAALEgHS3X78AAAgAElEQVR4nOzdd5OcR3og+OdJ97ry1d4A3fAAAZAcuqEZauVGM9LMGp12T6tYxe5F3AfRp9g/Li4uNkK7e4o7SaHQaGOlGY04ImeGbmhBwhLobqDRvrrs6zPz2T+a5IAYWFpQyl9ERVV115uZr6nqzqeezEQiYgBA8Ol82u2c+4f3+HN7w8/ww+efth66Rfk3P/+iroGP6r7T/b243XH7vHzUjru16ca232sZd9vfW/3+bsfsTvefxr2ej3up58bff9a23et5+Sw+7TF0n5uO4ziO4zjOP3f3+j/03f7fvrn/dKf6AAA4EtHLine truncated
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 506 KiB |
@@ -1,132 +0,0 @@
|
|||||||
<svg width="842" height="595" viewBox="0 0 842 595" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<g clip-path="url(#clip0_2006_114)">
|
|
||||||
<rect width="595" height="842" transform="translate(842 6.10352e-05) rotate(90)" fill="white"/>
|
|
||||||
<line x1="842.5" y1="3.00006" x2="-10.4999" y2="3.00006" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="28.0001" x2="-10.4999" y2="28.0001" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="53.0001" x2="-10.4999" y2="53.0001" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="78.0001" x2="-10.4999" y2="78.0001" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="103" x2="-10.4999" y2="103" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="128" x2="-10.4999" y2="128" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="153" x2="-10.4999" y2="153" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="178" x2="-10.4999" y2="178" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="203" x2="-10.4999" y2="203" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="228" x2="-10.4999" y2="228" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="253" x2="-10.4999" y2="253" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="278" x2="-10.4999" y2="278" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="303" x2="-10.4999" y2="303" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="328" x2="-10.4999" y2="328" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="353" x2="-10.4999" y2="353" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="378" x2="-10.4999" y2="378" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="403" x2="-10.4999" y2="403" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="428" x2="-10.4999" y2="428" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="453" x2="-10.4999" y2="453" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="478" x2="-10.4999" y2="478" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="503" x2="-10.4999" y2="503" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="528" x2="-10.4999" y2="528" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="553" x2="-10.4999" y2="553" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="842.5" y1="578" x2="-10.4999" y2="578" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="0.500026" y1="-0.499939" x2="0.500052" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="24.5" y1="-0.499939" x2="24.5001" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="48.5" y1="-0.499939" x2="48.5001" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="72.5001" y1="-0.499939" x2="72.5002" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="96.5001" y1="-0.499939" x2="96.5002" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="120.5" y1="-0.499939" x2="120.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="144.5" y1="-0.499939" x2="144.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="168.5" y1="-0.499939" x2="168.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="192.5" y1="-0.499939" x2="192.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="216.5" y1="-0.499939" x2="216.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="240.5" y1="-0.499939" x2="240.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="264.5" y1="-0.499939" x2="264.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="288.5" y1="-0.499939" x2="288.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="312.5" y1="-0.499939" x2="312.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="336.5" y1="-0.499939" x2="336.5" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="360.501" y1="-0.499939" x2="360.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="384.501" y1="-0.499939" x2="384.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="408.501" y1="-0.499939" x2="408.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="432.501" y1="-0.499939" x2="432.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="456.501" y1="-0.499939" x2="456.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="480.501" y1="-0.499939" x2="480.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="504.501" y1="-0.499939" x2="504.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="528.501" y1="-0.499939" x2="528.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="552.501" y1="-0.499939" x2="552.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="576.501" y1="-0.499939" x2="576.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="600.501" y1="-0.499939" x2="600.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="624.501" y1="-0.499939" x2="624.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="648.501" y1="-0.499939" x2="648.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="672.501" y1="-0.499939" x2="672.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="696.501" y1="-0.499939" x2="696.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="720.501" y1="-0.499939" x2="720.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="744.501" y1="-0.499939" x2="744.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="768.501" y1="-0.499939" x2="768.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="792.501" y1="-0.499939" x2="792.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="816.501" y1="-0.499939" x2="816.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<line x1="840.501" y1="-0.499939" x2="840.501" y2="595.501" stroke="#F4FBFF" stroke-width="2"/>
|
|
||||||
<g opacity="0.4" filter="url(#filter0_f_2006_114)">
|
|
||||||
<circle cx="12.5" cy="691.5" r="159" fill="#26A5EE"/>
|
|
||||||
</g>
|
|
||||||
<g filter="url(#filter1_f_2006_114)">
|
|
||||||
<circle cx="744" cy="607" r="159" fill="#26A5EE"/>
|
|
||||||
</g>
|
|
||||||
<g filter="url(#filter2_f_2006_114)">
|
|
||||||
<circle cx="80.5" cy="-22.4999" r="159" fill="white"/>
|
|
||||||
</g>
|
|
||||||
<rect x="30.5" y="40.5001" width="236" height="53" fill="url(#pattern0_2006_114)"/>
|
|
||||||
<rect x="275.5" y="127.5" width="37" height="251" rx="13.5" transform="rotate(90 275.5 127.5)" fill="url(#paint0_linear_2006_114)"/>
|
|
||||||
<rect x="275.5" y="127.5" width="37" height="251" rx="13.5" transform="rotate(90 275.5 127.5)" stroke="#E1ECFC"/>
|
|
||||||
<path d="M42.2 153.14C41.2267 153.14 40.3467 152.973 39.56 152.64C38.7867 152.307 38.1734 151.827 37.72 151.2C37.2667 150.573 37.0334 149.833 37.02 148.98H40.02C40.06 149.553 40.26 150.007 40.62 150.34C40.9934 150.673 41.5 150.84 42.14 150.84C42.7934 150.84 43.3067 150.687 43.68 150.38C44.0534 150.06 44.24 149.647 44.24 149.14C44.24 148.727 44.1134 148.387 43.86 148.12C43.6067 147.853 43.2867 147.647 42.9 147.5C42.5267 147.34 42.0067 147.167 41.34 146.98C40.4334 146.713 39.6934 146.453 39.12 14Line truncated
|
|
||||||
<path d="M36.54 195.6C38.02 195.6 39.3134 195.887 40.42 196.46C41.54 197.02 42.4 197.833 43 198.9C43.6134 199.953 43.92 201.187 43.92 202.6C43.92 204.013 43.6134 205.24 43 206.28C42.4 207.32 41.54 208.12 40.42 208.68C39.3134 209.227 38.02 209.5 36.54 209.5H32V195.6H36.54ZM36.54 207.64C38.1667 207.64 39.4134 207.2 40.28 206.32C41.1467 205.44 41.58 204.2 41.58 202.6C41.58 200.987 41.1467 199.727 40.28 198.82C39.4134 197.913 38.1667 197.46 36.54 197.46H34.28V207.64H36.54ZM46.9397 197.02C46.5264 19Line truncated
|
|
||||||
<path d="M606.364 497.5L603.9 493.148H602.844V497.5H600.604V486.332H604.796C605.66 486.332 606.396 486.487 607.004 486.796C607.612 487.095 608.065 487.505 608.364 488.028C608.673 488.54 608.828 489.116 608.828 489.756C608.828 490.492 608.615 491.159 608.188 491.756C607.761 492.343 607.127 492.748 606.284 492.972L608.956 497.5H606.364ZM602.844 491.468H604.716C605.324 491.468 605.777 491.324 606.076 491.036C606.375 490.737 606.524 490.327 606.524 489.804C606.524 489.292 606.375 488.897 606.076 48Line truncated
|
|
||||||
<path d="M599.944 511.312C599.944 510.496 600.132 509.764 600.508 509.116C600.892 508.468 601.408 507.964 602.056 507.604C602.712 507.236 603.428 507.052 604.204 507.052C605.092 507.052 605.88 507.272 606.568 507.712C607.264 508.144 607.768 508.76 608.08 509.56H606.436C606.22 509.12 605.92 508.792 605.536 508.576C605.152 508.36 604.708 508.252 604.204 508.252C603.652 508.252 603.16 508.376 602.728 508.624C602.296 508.872 601.956 509.228 601.708 509.692C601.468 510.156 601.348 510.696 601.348 51Line truncated
|
|
||||||
<path d="M400.452 495.372H396.004L395.268 497.5H392.916L396.932 486.316H399.54L403.556 497.5H401.188L400.452 495.372ZM399.844 493.58L398.228 488.908L396.612 493.58H399.844ZM406.977 485.66V497.5H404.737V485.66H406.977ZM410.006 487.58C409.612 487.58 409.281 487.457 409.014 487.212C408.758 486.956 408.63 486.641 408.63 486.268C408.63 485.895 408.758 485.585 409.014 485.34C409.281 485.084 409.612 484.956 410.006 484.956C410.401 484.956 410.726 485.084 410.982 485.34C411.249 485.585 411.382 485.895 Line truncated
|
|
||||||
<path d="M392.944 511.312C392.944 510.496 393.132 509.764 393.508 509.116C393.892 508.468 394.408 507.964 395.056 507.604C395.712 507.236 396.428 507.052 397.204 507.052C398.092 507.052 398.88 507.272 399.568 507.712C400.264 508.144 400.768 508.76 401.08 509.56H399.436C399.22 509.12 398.92 508.792 398.536 508.576C398.152 508.36 397.708 508.252 397.204 508.252C396.652 508.252 396.16 508.376 395.728 508.624C395.296 508.872 394.956 509.228 394.708 509.692C394.468 510.156 394.348 510.696 394.348 51Line truncated
|
|
||||||
<path d="M284.868 486.332V497.5H282.628V492.748H277.844V497.5H275.604V486.332H277.844V490.924H282.628V486.332H284.868ZM286.177 493.036C286.177 492.14 286.353 491.345 286.705 490.652C287.067 489.959 287.553 489.425 288.161 489.052C288.779 488.679 289.467 488.492 290.225 488.492C290.886 488.492 291.462 488.625 291.953 488.892C292.454 489.159 292.854 489.495 293.153 489.9V488.636H295.409V497.5H293.153V496.204C292.865 496.62 292.465 496.967 291.953 497.244C291.451 497.511 290.87 497.644 290.209 497Line truncated
|
|
||||||
<path d="M280.428 513.796H276.936L276.336 515.5H274.908L277.896 507.148H279.48L282.468 515.5H281.028L280.428 513.796ZM280.044 512.68L278.688 508.804L277.32 512.68H280.044ZM283.071 512.164C283.071 511.5 283.207 510.912 283.479 510.4C283.759 509.888 284.135 509.492 284.607 509.212C285.087 508.924 285.619 508.78 286.203 508.78C286.635 508.78 287.059 508.876 287.475 509.068C287.899 509.252 288.235 509.5 288.483 509.812V506.62H289.863V515.5H288.483V514.504C288.259 514.824 287.947 515.088 287.547 515Line truncated
|
|
||||||
<path d="M43.78 486.332V497.5H41.54V490.236L38.548 497.5H36.852L33.844 490.236V497.5H31.604V486.332H34.148L37.7 494.636L41.252 486.332H43.78ZM54.0427 488.636V497.5H51.7867V496.38C51.4987 496.764 51.12 497.068 50.6507 497.292C50.192 497.505 49.6907 497.612 49.1467 497.612C48.4533 497.612 47.84 497.468 47.3067 497.18C46.7733 496.881 46.352 496.449 46.0427 495.884C45.744 495.308 45.5947 494.625 45.5947 493.836V488.636H47.8347V493.516C47.8347 494.22 48.0107 494.764 48.3627 495.148C48.7147 495.521 4Line truncated
|
|
||||||
<path d="M36.428 513.796H32.936L32.336 515.5H30.908L33.896 507.148H35.48L38.468 515.5H37.028L36.428 513.796ZM36.044 512.68L34.688 508.804L33.32 512.68H36.044ZM39.0712 512.164C39.0712 511.5 39.2072 510.912 39.4792 510.4C39.7592 509.888 40.1352 509.492 40.6072 509.212C41.0872 508.924 41.6192 508.78 42.2032 508.78C42.6352 508.78 43.0592 508.876 43.4752 509.068C43.8992 509.252 44.2352 509.5 44.4832 509.812V506.62H45.8632V515.5H44.4832V514.504C44.2592 514.824 43.9472 515.088 43.5472 515.296C43.1552 Line truncated
|
|
||||||
<path d="M257.216 540.004C258.032 540.004 258.756 540.188 259.388 540.556C260.028 540.916 260.524 541.424 260.876 542.08C261.236 542.728 261.416 543.468 261.416 544.3C261.416 545.132 261.236 545.876 260.876 546.532C260.524 547.18 260.028 547.688 259.388 548.056C258.756 548.416 258.032 548.596 257.216 548.596C256.392 548.596 255.66 548.416 255.02 548.056C254.388 547.688 253.892 547.18 253.532 546.532C253.18 545.876 253.004 545.132 253.004 544.3C253.004 543.468 253.18 542.728 253.532 542.08C253.8Line truncated
|
|
||||||
<path d="M35.216 540.004C36.032 540.004 36.756 540.188 37.388 540.556C38.028 540.916 38.524 541.424 38.876 542.08C39.236 542.728 39.416 543.468 39.416 544.3C39.416 545.132 39.236 545.876 38.876 546.532C38.524 547.18 38.028 547.688 37.388 548.056C36.756 548.416 36.032 548.596 35.216 548.596C34.392 548.596 33.66 548.416 33.02 548.056C32.388 547.688 31.892 547.18 31.532 546.532C31.18 545.876 31.004 545.132 31.004 544.3C31.004 543.468 31.18 542.728 31.532 542.08C31.892 541.424 32.388 540.916 33.02 Line truncated
|
|
||||||
<rect x="782" y="176.5" width="200" height="200" rx="14.2857" transform="rotate(90 782 176.5)" fill="white"/>
|
|
||||||
<rect x="784.232" y="174.268" width="204.464" height="204.464" rx="16.5179" transform="rotate(90 784.232 174.268)" stroke="url(#paint2_linear_2006_114)" stroke-opacity="0.8" stroke-width="4.46429"/>
|
|
||||||
<rect x="594" y="23" width="198" height="108" fill="url(#pattern1_2006_114)"/>
|
|
||||||
</g>
|
|
||||||
<defs>
|
|
||||||
<filter id="filter0_f_2006_114" x="-486.5" y="192.5" width="998" height="998" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
||||||
<feGaussianBlur stdDeviation="170" result="effect1_foregroundBlur_2006_114"/>
|
|
||||||
</filter>
|
|
||||||
<filter id="filter1_f_2006_114" x="225" y="88" width="1038" height="1038" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
||||||
<feGaussianBlur stdDeviation="180" result="effect1_foregroundBlur_2006_114"/>
|
|
||||||
</filter>
|
|
||||||
<filter id="filter2_f_2006_114" x="-257.655" y="-360.655" width="676.31" height="676.31" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
||||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
|
||||||
<feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
|
|
||||||
<feGaussianBlur stdDeviation="89.5775" result="effect1_foregroundBlur_2006_114"/>
|
|
||||||
</filter>
|
|
||||||
<pattern id="pattern0_2006_114" patternContentUnits="objectBoundingBox" width="1" height="1">
|
|
||||||
<use xlink:href="#image0_2006_114" transform="matrix(0.000918274 0 0 0.00408892 0 -0.000892285)"/>
|
|
||||||
</pattern>
|
|
||||||
<pattern id="pattern1_2006_114" patternContentUnits="objectBoundingBox" width="1" height="1">
|
|
||||||
<use xlink:href="#image1_2006_114" transform="matrix(0.0014782 0 0 0.00271003 -0.000369549 0)"/>
|
|
||||||
</pattern>
|
|
||||||
<linearGradient id="paint0_linear_2006_114" x1="276" y1="253" x2="314" y2="253" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="white"/>
|
|
||||||
<stop offset="1" stop-color="#F9FDFF"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint1_linear_2006_114" x1="36" y1="146" x2="264" y2="146" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#22A8F1"/>
|
|
||||||
<stop offset="1" stop-color="#5CBFF5"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="paint2_linear_2006_114" x1="782" y1="276.5" x2="982" y2="276.5" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop stop-color="#E3F4FD"/>
|
|
||||||
<stop offset="1" stop-color="#1FAAEC"/>
|
|
||||||
</linearGradient>
|
|
||||||
<clipPath id="clip0_2006_114">
|
|
||||||
<rect width="595" height="842" fill="white" transform="translate(842 6.10352e-05) rotate(90)"/>
|
|
||||||
</clipPath>
|
|
||||||
<image id="image0_2006_114" width="1089" height="245" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAABEEAAAD1CAYAAAC/deukAAAACXBIWXMAAAsSAAALEgHS3X78AAAgAElEQVR4nOzdd5OcR3og+OdJ97ry1d4A3fAAAZAcuqEZauVGM9LMGp12T6tYxe5F3AfRp9g/Li4uNkK7e4o7SaHQaGOlGY04ImeGbmhBwhLobqDRvrrs6zPz2T+a5IAYWFpQyl9ERVV115uZr6nqzqeezEQiYgBA8Ol82u2c+4f3+HN7w8/ww+efth66Rfk3P/+iroGP6r7T/b243XH7vHzUjru16ca232sZd9vfW/3+bsfsTvefxr2ej3up58bff9a23et5+Sw+7TF0n5uO4ziO4zjOP3f3+j/03f7fvrn/dKf6AAA4Line truncated
|
|
||||||
<image id="image1_2006_114" width="677" height="369" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAqUAAAFxCAYAAABKn9GWAAAQAElEQVR4Aex9BYBUR7r1qWvtNu7GDDO4u0OAOHF32WTjJMTd3d0VgoXg7u4wwgDj7m7tff+veiDJZrP2v7f7suFe5uu6t7xOVU+dOl9PI0C7NAQ0BDQENAQ0BDQENAQ0BDQE/o8R0Ejp//EEaM1rCGgInAoIaGPUENAQ0BDQEPhHCGik9B8hpKVrCGgIaAhoCGgIaAhoCGgI/NsR+B+T0n97D7UGNAQ0BDQENAQ0BDQENAQ0BP7wCGik9A8/xdoANQQ0BP4ACGhD0BDQENAQ+MMjoJHSP/wUawPUENAQ0BDQENAQ0BDQEPj9I/B/T0p//xhpPdQQ0BDQENAQ0Line truncated
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 818 KiB |
@@ -3,7 +3,8 @@ import { useParams, useNavigate } from 'react-router';
|
|||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { decodeCertificateId } from '../../../utils/certificate';
|
import { decodeCertificateId } from '../../../utils/certificate';
|
||||||
import {
|
import {
|
||||||
useCertificatePublicData,
|
useTeamById,
|
||||||
|
useTeamSubmission,
|
||||||
useAuthStore,
|
useAuthStore,
|
||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
import QRCode from 'qrcode';
|
import QRCode from 'qrcode';
|
||||||
@@ -12,7 +13,6 @@ import html2canvas from 'html2canvas';
|
|||||||
interface DecodedCert {
|
interface DecodedCert {
|
||||||
teamId: string;
|
teamId: string;
|
||||||
submissionId: string;
|
submissionId: string;
|
||||||
userId: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const CertificatePage: FC = (): ReactElement => {
|
const CertificatePage: FC = (): ReactElement => {
|
||||||
@@ -41,16 +41,10 @@ const CertificatePage: FC = (): ReactElement => {
|
|||||||
}
|
}
|
||||||
}, [certId]);
|
}, [certId]);
|
||||||
|
|
||||||
// Fetch certificate data using the new endpoint
|
|
||||||
const { data: certificateData, isLoading: isLoadingCertificate } =
|
|
||||||
useCertificatePublicData(decodedInfo?.userId || '', !!decodedInfo?.userId);
|
|
||||||
|
|
||||||
// Generate QR Code
|
// Generate QR Code
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (certId) {
|
if (certId) {
|
||||||
// Use encodeURIComponent to properly encode the certId for the URL
|
const certificateUrl = `${window.location.origin}/certificate/${certId}`;
|
||||||
const encodedCertId = encodeURIComponent(certId);
|
|
||||||
const certificateUrl = `${window.location.origin}/certificate/${encodedCertId}`;
|
|
||||||
QRCode.toDataURL(certificateUrl, {
|
QRCode.toDataURL(certificateUrl, {
|
||||||
width: 200,
|
width: 200,
|
||||||
margin: 1,
|
margin: 1,
|
||||||
@@ -64,18 +58,18 @@ const CertificatePage: FC = (): ReactElement => {
|
|||||||
}
|
}
|
||||||
}, [certId]);
|
}, [certId]);
|
||||||
|
|
||||||
const certificate = certificateData?.data;
|
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(
|
||||||
const team = certificate?.team;
|
decodedInfo?.teamId || '',
|
||||||
const submission = certificate?.submission;
|
!!decodedInfo?.teamId
|
||||||
const certificateUser = certificate?.user;
|
);
|
||||||
|
const { data: submissionData, isLoading: isLoadingSubmission } =
|
||||||
|
useTeamSubmission(decodedInfo?.teamId || '', !!decodedInfo?.teamId);
|
||||||
|
|
||||||
const isLoading = (!decodedInfo && !error) || isLoadingCertificate;
|
const team = teamData?.data;
|
||||||
|
const submission = submissionData?.data;
|
||||||
|
|
||||||
// Certificate name from the user data
|
const isLoading =
|
||||||
const certificateName = certificateUser?.fullname;
|
(!decodedInfo && !error) || isLoadingTeam || isLoadingSubmission;
|
||||||
|
|
||||||
// Check if current user is viewing their own certificate (team member)
|
|
||||||
const isTeamMember = session?.user?.id === decodedInfo?.userId;
|
|
||||||
|
|
||||||
// Dynamic font sizing: shrink by 2px if height exceeds 80px
|
// Dynamic font sizing: shrink by 2px if height exceeds 80px
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -104,7 +98,7 @@ const CertificatePage: FC = (): ReactElement => {
|
|||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
return () => clearTimeout(timer);
|
||||||
}, [team?.name, certificateName]);
|
}, [team?.name, session?.user?.fullname]);
|
||||||
|
|
||||||
// Generate certificate canvas screenshot
|
// Generate certificate canvas screenshot
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -113,22 +107,19 @@ const CertificatePage: FC = (): ReactElement => {
|
|||||||
|
|
||||||
setIsGenerating(true);
|
setIsGenerating(true);
|
||||||
try {
|
try {
|
||||||
// Wait longer for fonts and images to load properly
|
// Wait a bit for fonts and images to load
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
|
||||||
const canvas = await html2canvas(certificateRef.current, {
|
const canvas = await html2canvas(certificateRef.current, {
|
||||||
scale: 4,
|
scale: 2,
|
||||||
useCORS: true,
|
useCORS: true,
|
||||||
backgroundColor: '#ffffff',
|
backgroundColor: '#ffffff',
|
||||||
logging: false,
|
logging: false,
|
||||||
width: 1000,
|
width: 1000,
|
||||||
height: (1000 * 2480) / 3508,
|
height: (1000 * 2480) / 3508,
|
||||||
allowTaint: true,
|
|
||||||
imageTimeout: 0,
|
|
||||||
removeContainer: true,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const imageUrl = canvas.toDataURL('image/png', 1.0);
|
const imageUrl = canvas.toDataURL('image/png');
|
||||||
setCertificateImage(imageUrl);
|
setCertificateImage(imageUrl);
|
||||||
setShowTemplate(false);
|
setShowTemplate(false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -139,7 +130,7 @@ const CertificatePage: FC = (): ReactElement => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
generateCertificate();
|
generateCertificate();
|
||||||
}, [team, submission, qrCodeUrl]);
|
}, [team, submission, qrCodeUrl, session?.user?.fullname]);
|
||||||
|
|
||||||
// Download certificate
|
// Download certificate
|
||||||
const handleDownloadCertificate = () => {
|
const handleDownloadCertificate = () => {
|
||||||
@@ -211,7 +202,7 @@ const CertificatePage: FC = (): ReactElement => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!certificateUser) {
|
if (!submission || submission.id !== decodedInfo?.submissionId) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
|
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||||
<div className="text-6xl mb-4">📄</div>
|
<div className="text-6xl mb-4">📄</div>
|
||||||
@@ -219,7 +210,7 @@ const CertificatePage: FC = (): ReactElement => {
|
|||||||
Certificate Not Found
|
Certificate Not Found
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
||||||
The user associated with this certificate could not be found.
|
The submission associated with this certificate could not be found.
|
||||||
</p>
|
</p>
|
||||||
<Button onClick={() => navigate('/')}>Back to Home</Button>
|
<Button onClick={() => navigate('/')}>Back to Home</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -287,14 +278,14 @@ const CertificatePage: FC = (): ReactElement => {
|
|||||||
{team?.name}
|
{team?.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{team && isTeamMember && (
|
<Button
|
||||||
<Button
|
variant="secondary"
|
||||||
variant="secondary"
|
onClick={() =>
|
||||||
onClick={() => navigate(`/teams/${team.id}/submission`)}
|
navigate(`/teams/${decodedInfo?.teamId}/submission`)
|
||||||
>
|
}
|
||||||
Back to Submission
|
>
|
||||||
</Button>
|
Back to Submission
|
||||||
)}
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -314,30 +305,57 @@ const CertificatePage: FC = (): ReactElement => {
|
|||||||
id="certificate-template"
|
id="certificate-template"
|
||||||
style={{
|
style={{
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
backgroundImage: 'url(/images/blank_cert.svg)',
|
backgroundImage: 'url(/images/blank_cert.png)',
|
||||||
backgroundSize: 'cover',
|
backgroundSize: 'cover',
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: 'center',
|
||||||
width: '1000px',
|
width: '1000px',
|
||||||
height: `${(1000 * 2480) / 3508}px`,
|
height: `${(1000 * 2480) / 3508}px`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Team Name - positioned in middle between "Diberikan Kepada" and "Telah Berpartisipasi" */}
|
{/* User Name (from session) */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '41%',
|
top: '40%',
|
||||||
left: '3.5%',
|
left: '50%',
|
||||||
width: '55%',
|
transform: 'translateX(-50%)',
|
||||||
|
width: '80%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h3
|
||||||
|
ref={userNameRef}
|
||||||
|
style={{
|
||||||
|
fontWeight: 'bold',
|
||||||
|
color: '#111827',
|
||||||
|
textAlign: 'center',
|
||||||
|
fontSize: userNameFontSize,
|
||||||
|
lineHeight: '1.2',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
textShadow: '0 1px 2px rgba(0,0,0,0.1)',
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{session?.user?.fullname || 'N/A'}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Team Name */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
top: '46%',
|
||||||
|
left: '50%',
|
||||||
|
transform: 'translateX(-50%)',
|
||||||
|
width: '70%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3
|
<h3
|
||||||
ref={teamNameRef}
|
ref={teamNameRef}
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'Poppins, sans-serif',
|
fontWeight: '600',
|
||||||
fontWeight: 'bold',
|
color: '#1f2937',
|
||||||
color: '#59bef5',
|
textAlign: 'center',
|
||||||
textAlign: 'left',
|
fontSize: teamNameFontSize,
|
||||||
fontSize: '32px',
|
|
||||||
lineHeight: '1.2',
|
lineHeight: '1.2',
|
||||||
wordBreak: 'break-word',
|
wordBreak: 'break-word',
|
||||||
margin: 0,
|
margin: 0,
|
||||||
@@ -347,53 +365,84 @@ const CertificatePage: FC = (): ReactElement => {
|
|||||||
</h3>
|
</h3>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* User Name - positioned below team name */}
|
{/* Participation Text */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '45%',
|
top: '60%',
|
||||||
left: '3.5%',
|
left: '50%',
|
||||||
width: '55%',
|
transform: 'translateX(-50%)',
|
||||||
|
width: '70%',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<h3
|
<p
|
||||||
ref={userNameRef}
|
|
||||||
style={{
|
style={{
|
||||||
fontFamily: 'Poppins, sans-serif',
|
textAlign: 'center',
|
||||||
fontWeight: 'bold',
|
color: '#374151',
|
||||||
color: '#59bef5',
|
fontSize: '18px',
|
||||||
textAlign: 'left',
|
fontWeight: '500',
|
||||||
fontSize: '40px',
|
|
||||||
lineHeight: '1.2',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
margin: 0,
|
margin: 0,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{certificateName || 'N/A'}
|
<span style={{ fontWeight: 'bold' }}>
|
||||||
</h3>
|
Peserta Hackathon IMPHNEN x KOLOSAL AI
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* QR Code - positioned in the white box area */}
|
{/* QR Code */}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
top: '33%',
|
bottom: '8%',
|
||||||
right: '9.3%',
|
left: '8%',
|
||||||
width: '190px',
|
|
||||||
height: '190px',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{qrCodeUrl && (
|
{qrCodeUrl && (
|
||||||
<img
|
<div
|
||||||
src={qrCodeUrl}
|
style={{
|
||||||
alt="Certificate QR Code"
|
backgroundColor: '#ffffff',
|
||||||
style={{ width: '190px', height: '190px', display: 'block' }}
|
padding: '8px',
|
||||||
/>
|
borderRadius: '4px',
|
||||||
|
boxShadow: '0 4px 6px rgba(0,0,0,0.1)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={qrCodeUrl}
|
||||||
|
alt="Certificate QR Code"
|
||||||
|
style={{ width: '96px', height: '96px' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Date */}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
bottom: '8%',
|
||||||
|
right: '8%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: '14px',
|
||||||
|
color: '#374151',
|
||||||
|
margin: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{submission.submitted_at
|
||||||
|
? new Date(submission.submitted_at).toLocaleDateString(
|
||||||
|
'id-ID',
|
||||||
|
{
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
}
|
||||||
|
)
|
||||||
|
: 'N/A'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -420,35 +469,33 @@ const CertificatePage: FC = (): ReactElement => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Actions */}
|
{/* Actions */}
|
||||||
{isTeamMember && (
|
<div className="bg-gray-50 dark:bg-gray-800 p-6 grid grid-cols-2 xl:grid-cols-3 gap-3 justify-center no-print">
|
||||||
<div className="bg-gray-50 dark:bg-gray-900 p-6 grid grid-cols-2 xl:grid-cols-3 gap-3 justify-center no-print">
|
<Button
|
||||||
<Button
|
variant="secondary"
|
||||||
variant="secondary"
|
onClick={handleDownloadCertificate}
|
||||||
onClick={handleDownloadCertificate}
|
className="flex items-center gap-2"
|
||||||
className="flex items-center gap-2"
|
disabled={isGenerating}
|
||||||
disabled={isGenerating}
|
>
|
||||||
>
|
{isGenerating ? '⏳ Generating...' : '📥 Download'}
|
||||||
{isGenerating ? '⏳ Generating...' : '📥 Download'}
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
<Button
|
variant="secondary"
|
||||||
variant="secondary"
|
onClick={handlePrintCertificate}
|
||||||
onClick={handlePrintCertificate}
|
className="flex items-center gap-2"
|
||||||
className="flex items-center gap-2"
|
disabled={isGenerating}
|
||||||
disabled={isGenerating}
|
>
|
||||||
>
|
{isGenerating ? '⏳ Generating...' : '🖨️ Print'}
|
||||||
{isGenerating ? '⏳ Generating...' : '🖨️ Print'}
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
{team && (
|
onClick={() =>
|
||||||
<Button
|
navigate(`/teams/${decodedInfo?.teamId}/submission`)
|
||||||
onClick={() => navigate(`/teams/${team.id}/submission`)}
|
}
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
className="col-span-2 flex items-center gap-2 xl:col-span-1"
|
className="col-span-2 flex items-center gap-2 xl:col-span-1"
|
||||||
>
|
>
|
||||||
View Submission
|
View Submission
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
</div>
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Info Box */}
|
{/* Info Box */}
|
||||||
|
|||||||
@@ -1,605 +0,0 @@
|
|||||||
import { FC, ReactElement, useEffect, useMemo, useRef, useState } from 'react';
|
|
||||||
import { useNavigate, useParams } from 'react-router';
|
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
|
||||||
import { Icon } from '@iconify/react';
|
|
||||||
import { decodeWinnerCertificateId } from '../../../../utils/certificate';
|
|
||||||
import {
|
|
||||||
useAuthStore,
|
|
||||||
useMyTeams,
|
|
||||||
useTeamById,
|
|
||||||
useTeamSubmission,
|
|
||||||
useWinners,
|
|
||||||
} from '@imphnen-frontend-service/service';
|
|
||||||
import QRCode from 'qrcode';
|
|
||||||
import html2canvas from 'html2canvas';
|
|
||||||
|
|
||||||
type WinnerEntry = {
|
|
||||||
team_id: string;
|
|
||||||
rank: number;
|
|
||||||
team?: {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const formatOrdinalRank = (rank: number): string => {
|
|
||||||
const mod100 = rank % 100;
|
|
||||||
if (mod100 >= 11 && mod100 <= 13) return `${rank}th`;
|
|
||||||
|
|
||||||
switch (rank % 10) {
|
|
||||||
case 1:
|
|
||||||
return `${rank}st`;
|
|
||||||
case 2:
|
|
||||||
return `${rank}nd`;
|
|
||||||
case 3:
|
|
||||||
return `${rank}rd`;
|
|
||||||
default:
|
|
||||||
return `${rank}th`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Keep the template size aligned with the SVG native size (842x595) using an integer multiplier.
|
|
||||||
// This reduces sub-pixel scaling artifacts (blur) on thin lines when rasterizing with html2canvas.
|
|
||||||
const CERT_WIDTH = 842 * 2;
|
|
||||||
const CERT_HEIGHT = 595 * 2;
|
|
||||||
|
|
||||||
// Balance between output sharpness and file size.
|
|
||||||
// Output resolution will be (CERT_WIDTH * EXPORT_SCALE) x (CERT_HEIGHT * EXPORT_SCALE).
|
|
||||||
const EXPORT_SCALE = 2;
|
|
||||||
|
|
||||||
// The original layout was tuned around a ~1000px-wide template.
|
|
||||||
// We keep the same visual proportions by scaling fixed pixel values.
|
|
||||||
const LAYOUT_BASE_WIDTH = 1000;
|
|
||||||
const LAYOUT_SCALE = CERT_WIDTH / LAYOUT_BASE_WIDTH;
|
|
||||||
const s = (px: number) => Math.round(px * LAYOUT_SCALE);
|
|
||||||
|
|
||||||
const CertificateWinnerPage: FC = (): ReactElement => {
|
|
||||||
const { certId } = useParams<{ certId: string }>();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const { session } = useAuthStore();
|
|
||||||
const { data: myTeamsData } = useMyTeams();
|
|
||||||
const {
|
|
||||||
data: winnersResponse,
|
|
||||||
isLoading: isLoadingWinners,
|
|
||||||
isError: isWinnersError,
|
|
||||||
} = useWinners();
|
|
||||||
|
|
||||||
const [decodedTeamId, setDecodedTeamId] = useState<string>('');
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
|
|
||||||
const [qrCodeUrl, setQrCodeUrl] = useState<string>('');
|
|
||||||
const certificateRef = useRef<HTMLDivElement>(null);
|
|
||||||
const [isGenerating, setIsGenerating] = useState(false);
|
|
||||||
const [certificateImage, setCertificateImage] = useState<string>('');
|
|
||||||
const [showTemplate, setShowTemplate] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!certId) return;
|
|
||||||
|
|
||||||
decodeWinnerCertificateId(certId)
|
|
||||||
.then((decoded) => setDecodedTeamId(decoded.teamId))
|
|
||||||
.catch(() => setError('Invalid certificate ID'));
|
|
||||||
}, [certId]);
|
|
||||||
|
|
||||||
const winners = useMemo(
|
|
||||||
() => (winnersResponse?.data || []) as WinnerEntry[],
|
|
||||||
[winnersResponse?.data]
|
|
||||||
);
|
|
||||||
const winnerEntry = useMemo(() => {
|
|
||||||
if (!decodedTeamId) return undefined;
|
|
||||||
return winners.find((w) => w.team_id === decodedTeamId);
|
|
||||||
}, [decodedTeamId, winners]);
|
|
||||||
|
|
||||||
const rankLabel = useMemo(() => {
|
|
||||||
if (!winnerEntry?.rank) return '';
|
|
||||||
return formatOrdinalRank(winnerEntry.rank);
|
|
||||||
}, [winnerEntry?.rank]);
|
|
||||||
|
|
||||||
const { data: teamData, isLoading: isLoadingTeam } = useTeamById(
|
|
||||||
decodedTeamId,
|
|
||||||
!!decodedTeamId
|
|
||||||
);
|
|
||||||
|
|
||||||
const { data: submissionData, isLoading: isLoadingSubmission } =
|
|
||||||
useTeamSubmission(decodedTeamId, !!decodedTeamId);
|
|
||||||
|
|
||||||
const team = teamData?.data;
|
|
||||||
const submission = submissionData?.data;
|
|
||||||
const submissionName = submission?.project_name || '(Submission unavailable)';
|
|
||||||
|
|
||||||
const memberNames = useMemo(() => {
|
|
||||||
const members = team?.members || [];
|
|
||||||
return members
|
|
||||||
.map((m) => m.user?.fullname)
|
|
||||||
.filter((name): name is string => !!name);
|
|
||||||
}, [team?.members]);
|
|
||||||
|
|
||||||
const isWinnerTeam = !!winnerEntry;
|
|
||||||
const isTeamMember =
|
|
||||||
!!session?.user?.id &&
|
|
||||||
!!decodedTeamId &&
|
|
||||||
(myTeamsData?.data || []).some(
|
|
||||||
(t) => (t as { id?: string } | null | undefined)?.id === decodedTeamId
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate QR Code (public link)
|
|
||||||
useEffect(() => {
|
|
||||||
if (!certId) return;
|
|
||||||
|
|
||||||
const encodedCertId = encodeURIComponent(certId);
|
|
||||||
const certificateUrl = `${window.location.origin}/certificate/winner/${encodedCertId}`;
|
|
||||||
|
|
||||||
QRCode.toDataURL(certificateUrl, {
|
|
||||||
width: s(200),
|
|
||||||
margin: 1,
|
|
||||||
color: {
|
|
||||||
dark: '#000000',
|
|
||||||
light: '#ffffff',
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then(setQrCodeUrl)
|
|
||||||
.catch((err) => console.error('QR Code generation failed:', err));
|
|
||||||
}, [certId]);
|
|
||||||
|
|
||||||
// Generate certificate canvas screenshot
|
|
||||||
useEffect(() => {
|
|
||||||
const generateCertificate = async () => {
|
|
||||||
if (!certificateRef.current) return;
|
|
||||||
if (!team?.name) return;
|
|
||||||
if (!qrCodeUrl) return;
|
|
||||||
if (!winnerEntry?.rank) return;
|
|
||||||
if (isLoadingSubmission) return;
|
|
||||||
|
|
||||||
setIsGenerating(true);
|
|
||||||
try {
|
|
||||||
setShowTemplate(true);
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
||||||
|
|
||||||
const canvas = await html2canvas(certificateRef.current, {
|
|
||||||
scale: EXPORT_SCALE,
|
|
||||||
useCORS: true,
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
logging: false,
|
|
||||||
width: CERT_WIDTH,
|
|
||||||
height: CERT_HEIGHT,
|
|
||||||
allowTaint: true,
|
|
||||||
imageTimeout: 0,
|
|
||||||
removeContainer: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const imageUrl = canvas.toDataURL('image/png', 1.0);
|
|
||||||
setCertificateImage(imageUrl);
|
|
||||||
setShowTemplate(false);
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Failed to generate certificate:', e);
|
|
||||||
} finally {
|
|
||||||
setIsGenerating(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
generateCertificate();
|
|
||||||
}, [
|
|
||||||
team?.name,
|
|
||||||
memberNames,
|
|
||||||
qrCodeUrl,
|
|
||||||
winnerEntry?.rank,
|
|
||||||
isLoadingSubmission,
|
|
||||||
submissionName,
|
|
||||||
]);
|
|
||||||
|
|
||||||
const handleDownloadCertificate = () => {
|
|
||||||
if (!certificateImage) return;
|
|
||||||
|
|
||||||
const link = document.createElement('a');
|
|
||||||
link.href = certificateImage;
|
|
||||||
link.download = `winner-certificate-${team?.name || 'hackathon'}.png`;
|
|
||||||
link.click();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePrintCertificate = () => {
|
|
||||||
if (!certificateImage) return;
|
|
||||||
|
|
||||||
const printWindow = window.open('', '_blank');
|
|
||||||
if (!printWindow) return;
|
|
||||||
|
|
||||||
printWindow.document.write(`
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<title>Certificate - ${team?.name}</title>
|
|
||||||
<style>
|
|
||||||
body { margin: 0; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
|
|
||||||
img { max-width: 100%; height: auto; }
|
|
||||||
@media print {
|
|
||||||
@page { size: A4 landscape; margin: 0; }
|
|
||||||
body { margin: 0; }
|
|
||||||
img { width: 100%; height: auto; }
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<img src="${certificateImage}" />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`);
|
|
||||||
printWindow.document.close();
|
|
||||||
printWindow.onload = () => {
|
|
||||||
printWindow.print();
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
if (error || !certId) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
|
||||||
Invalid Certificate
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
||||||
{error || 'The certificate ID is invalid or malformed.'}
|
|
||||||
</p>
|
|
||||||
<Button onClick={() => navigate('/')}>Back to Home</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!decodedTeamId || isLoadingTeam) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
||||||
<div className="text-gray-600 dark:text-gray-400">
|
|
||||||
Loading certificate...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoadingWinners) {
|
|
||||||
return (
|
|
||||||
<div className="flex items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
||||||
<div className="text-gray-600 dark:text-gray-400">
|
|
||||||
Loading winners...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isWinnersError) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
|
||||||
Unable to Load Winners
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
||||||
Please try again later.
|
|
||||||
</p>
|
|
||||||
<Button onClick={() => navigate('/')}>Back to Home</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!isWinnerTeam) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
|
||||||
Certificate Not Found
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
||||||
This team is not listed as a hackathon winner.
|
|
||||||
</p>
|
|
||||||
<Button onClick={() => navigate('/')}>Back to Home</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!team?.name) {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-50 dark:bg-gray-950">
|
|
||||||
<h2 className="text-2xl font-bold text-gray-900 dark:text-white mb-4">
|
|
||||||
Team Not Found
|
|
||||||
</h2>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mb-6">
|
|
||||||
The team associated with this certificate could not be loaded.
|
|
||||||
</p>
|
|
||||||
<Button onClick={() => navigate('/')}>Back to Home</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
|
|
||||||
{/* Header */}
|
|
||||||
<div className="bg-white dark:bg-gray-900 border-b dark:border-gray-700 no-print">
|
|
||||||
<div className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div>
|
|
||||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white">
|
|
||||||
Winner Certificate
|
|
||||||
</h1>
|
|
||||||
<p className="text-gray-600 dark:text-gray-400 mt-1">
|
|
||||||
{team.name}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{isTeamMember && (
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => navigate('/dashboard')}
|
|
||||||
>
|
|
||||||
Back to Dashboard
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
className="max-w-6xl mx-auto px-4 sm:px-6 lg:px-8 py-12"
|
|
||||||
id="certificate-wrapper"
|
|
||||||
>
|
|
||||||
{/* Hidden Template for Canvas Generation */}
|
|
||||||
<div
|
|
||||||
className={showTemplate ? 'block' : 'hidden'}
|
|
||||||
style={{ position: 'absolute', left: '-9999px' }}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
ref={certificateRef}
|
|
||||||
id="certificate-template"
|
|
||||||
style={{
|
|
||||||
position: 'relative',
|
|
||||||
backgroundImage: 'url(/images/blank_winner_cert.svg)',
|
|
||||||
backgroundSize: '100% 100%',
|
|
||||||
backgroundRepeat: 'no-repeat',
|
|
||||||
backgroundPosition: 'center',
|
|
||||||
width: `${CERT_WIDTH}px`,
|
|
||||||
height: `${CERT_HEIGHT}px`,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<style>{`
|
|
||||||
#winner-members li::marker {
|
|
||||||
color: #59bef5;
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
|
|
||||||
{/* Team Name */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '35%',
|
|
||||||
left: '3.5%',
|
|
||||||
width: '55%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h3
|
|
||||||
style={{
|
|
||||||
fontFamily: 'Poppins, sans-serif',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
color: '#59bef5',
|
|
||||||
textAlign: 'left',
|
|
||||||
fontSize: `${s(28)}px`,
|
|
||||||
lineHeight: '1.2',
|
|
||||||
wordBreak: 'break-word',
|
|
||||||
margin: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{team.name}
|
|
||||||
</h3>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Members list */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '40.5%',
|
|
||||||
left: '3.5%',
|
|
||||||
width: '55%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<ul
|
|
||||||
id="winner-members"
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
fontFamily: 'Poppins, sans-serif',
|
|
||||||
fontSize: `${s(18)}px`,
|
|
||||||
lineHeight: '1.35',
|
|
||||||
color: '#59bef5',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{(memberNames.length
|
|
||||||
? memberNames
|
|
||||||
: ['(Members unavailable)']
|
|
||||||
).map((name) => (
|
|
||||||
<li key={name}>• {name}</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Award text */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '60%',
|
|
||||||
left: '3.5%',
|
|
||||||
width: '60%',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<p
|
|
||||||
style={{
|
|
||||||
margin: 0,
|
|
||||||
fontFamily: 'Poppins, sans-serif',
|
|
||||||
fontSize: `${s(18)}px`,
|
|
||||||
lineHeight: '1.35',
|
|
||||||
color: '#6B6B6B',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Diberikan sebagai penghargaan atas pencapaian meraih
|
|
||||||
<br />
|
|
||||||
<b>JUARA {winnerEntry.rank}</b> pada Hackathon IMPHNEN x
|
|
||||||
Kolosal.ai
|
|
||||||
<br />
|
|
||||||
<span>
|
|
||||||
dengan nama project: <b>{submissionName}</b>
|
|
||||||
</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Rank badge */}
|
|
||||||
{!!rankLabel && (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '8%',
|
|
||||||
right: '8.5%',
|
|
||||||
width: `${s(190)}px`,
|
|
||||||
display: 'flex',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
display: 'inline-flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
padding: `${s(6)}px ${s(16)}px`,
|
|
||||||
textAlign: 'center',
|
|
||||||
fontFamily: 'Poppins, sans-serif',
|
|
||||||
fontWeight: 700,
|
|
||||||
color: '#78350F',
|
|
||||||
fontSize: `${s(24)}px`,
|
|
||||||
lineHeight: '1',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{rankLabel}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* QR Code (same placement as existing certificate page) */}
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '33%',
|
|
||||||
right: '9.3%',
|
|
||||||
width: `${s(190)}px`,
|
|
||||||
height: `${s(190)}px`,
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{qrCodeUrl && (
|
|
||||||
<img
|
|
||||||
src={qrCodeUrl}
|
|
||||||
alt="Certificate QR Code"
|
|
||||||
style={{
|
|
||||||
width: `${s(190)}px`,
|
|
||||||
height: `${s(190)}px`,
|
|
||||||
display: 'block',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Display Certificate Image */}
|
|
||||||
<div className="bg-white dark:bg-gray-900 rounded-lg shadow-xl dark:shadow-gray-950/50 overflow-hidden p-2">
|
|
||||||
{isGenerating && (
|
|
||||||
<div className="flex items-center justify-center p-12">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600 mx-auto mb-4"></div>
|
|
||||||
<div className="text-gray-600 dark:text-gray-400">
|
|
||||||
Generating certificate...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{certificateImage && !isGenerating && (
|
|
||||||
<img
|
|
||||||
src={certificateImage}
|
|
||||||
alt="Winner Certificate"
|
|
||||||
className="w-full h-auto"
|
|
||||||
style={{ maxWidth: '100%', height: 'auto' }}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
{isTeamMember && (
|
|
||||||
<div className="bg-gray-50 dark:bg-gray-900 p-6 grid grid-cols-2 xl:grid-cols-3 gap-3 justify-center no-print">
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={handleDownloadCertificate}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
disabled={isGenerating}
|
|
||||||
>
|
|
||||||
{isGenerating ? (
|
|
||||||
<>
|
|
||||||
<Icon
|
|
||||||
icon="svg-spinners:ring-resize"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
/>
|
|
||||||
Generating...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon
|
|
||||||
icon="heroicons:arrow-down-tray"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
/>
|
|
||||||
Download
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="secondary"
|
|
||||||
onClick={handlePrintCertificate}
|
|
||||||
className="flex items-center gap-2"
|
|
||||||
disabled={isGenerating}
|
|
||||||
>
|
|
||||||
{isGenerating ? (
|
|
||||||
<>
|
|
||||||
<Icon
|
|
||||||
icon="svg-spinners:ring-resize"
|
|
||||||
width="18"
|
|
||||||
height="18"
|
|
||||||
/>
|
|
||||||
Generating...
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<Icon icon="mdi:printer" width="18" height="18" />
|
|
||||||
Print
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={() => navigate('/dashboard')}
|
|
||||||
variant="secondary"
|
|
||||||
className="col-span-2 flex items-center gap-2 xl:col-span-1"
|
|
||||||
>
|
|
||||||
Back to Dashboard
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Info Box */}
|
|
||||||
<div className="mt-8 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-6 no-print">
|
|
||||||
<h3 className="font-bold text-blue-900 dark:text-blue-100 mb-2">
|
|
||||||
Certificate Information
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-blue-800 dark:text-blue-200">
|
|
||||||
This certificate is a digital record of your hackathon achievement.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CertificateWinnerPage;
|
|
||||||
@@ -1,17 +1,15 @@
|
|||||||
import { FC, ReactElement, useEffect, useMemo, useState } from 'react';
|
import { FC, ReactElement, useEffect, useState } from 'react';
|
||||||
import { Link, useNavigate } from 'react-router';
|
import { Link } from 'react-router';
|
||||||
import {
|
import {
|
||||||
useMyTeams,
|
useMyTeams,
|
||||||
useMyInvitations,
|
useMyInvitations,
|
||||||
useRespondToInvitation,
|
useRespondToInvitation,
|
||||||
useAuthStore,
|
useAuthStore,
|
||||||
useWinners,
|
|
||||||
} from '@imphnen-frontend-service/service';
|
} from '@imphnen-frontend-service/service';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { Icon } from '@iconify/react';
|
import { Icon } from '@iconify/react';
|
||||||
import ProfilePage from '../profile/page';
|
import ProfilePage from '../profile/page';
|
||||||
import { encodeWinnerCertificateId } from '../../utils/certificate';
|
|
||||||
|
|
||||||
// Team features deadline: 2025-11-30 23:59:00 WIB (UTC+7)
|
// Team features deadline: 2025-11-30 23:59:00 WIB (UTC+7)
|
||||||
const TEAM_FEATURES_DEADLINE = new Date('2025-11-30T16:59:00Z');
|
const TEAM_FEATURES_DEADLINE = new Date('2025-11-30T16:59:00Z');
|
||||||
@@ -41,7 +39,6 @@ type Invitation = {
|
|||||||
|
|
||||||
const DashboardPage: FC = (): ReactElement => {
|
const DashboardPage: FC = (): ReactElement => {
|
||||||
const { session } = useAuthStore();
|
const { session } = useAuthStore();
|
||||||
const navigate = useNavigate();
|
|
||||||
const [showProfileModal, setShowProfileModal] = useState(false);
|
const [showProfileModal, setShowProfileModal] = useState(false);
|
||||||
const [timeLeft, setTimeLeft] = useState<{
|
const [timeLeft, setTimeLeft] = useState<{
|
||||||
days: number;
|
days: number;
|
||||||
@@ -94,7 +91,6 @@ const DashboardPage: FC = (): ReactElement => {
|
|||||||
}
|
}
|
||||||
}, [showProfileModal]);
|
}, [showProfileModal]);
|
||||||
const { data: teamsData } = useMyTeams();
|
const { data: teamsData } = useMyTeams();
|
||||||
const { data: winnersResponse } = useWinners();
|
|
||||||
const { data: invitationsData } = useMyInvitations();
|
const { data: invitationsData } = useMyInvitations();
|
||||||
const { mutateAsync: respondToInvitation } = useRespondToInvitation();
|
const { mutateAsync: respondToInvitation } = useRespondToInvitation();
|
||||||
|
|
||||||
@@ -103,13 +99,6 @@ const DashboardPage: FC = (): ReactElement => {
|
|||||||
const invitations: Invitation[] = (invitationsData?.data ||
|
const invitations: Invitation[] = (invitationsData?.data ||
|
||||||
[]) as Invitation[];
|
[]) as Invitation[];
|
||||||
|
|
||||||
const winnerEntry = useMemo(() => {
|
|
||||||
const team = (myTeams[0] as { id?: string } | null | undefined) || null;
|
|
||||||
const winners = winnersResponse?.data || [];
|
|
||||||
if (!team?.id) return null;
|
|
||||||
return winners.find((w) => w.team_id === team.id) || null;
|
|
||||||
}, [myTeams, winnersResponse?.data]);
|
|
||||||
|
|
||||||
const handleAcceptInvitation = async (invitationId: string) => {
|
const handleAcceptInvitation = async (invitationId: string) => {
|
||||||
try {
|
try {
|
||||||
await respondToInvitation({ invitationId, action: 'accept' });
|
await respondToInvitation({ invitationId, action: 'accept' });
|
||||||
@@ -145,37 +134,6 @@ const DashboardPage: FC = (): ReactElement => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Winner Banner */}
|
|
||||||
{winnerEntry && myTeams.length > 0 && (
|
|
||||||
<div className="mb-8 bg-amber-50 dark:bg-amber-900/20 border-2 border-amber-400 dark:border-amber-500 rounded-lg p-6">
|
|
||||||
<div className="flex items-center justify-between flex-wrap gap-4">
|
|
||||||
<div className="flex items-center space-x-3 flex-1 min-w-0">
|
|
||||||
<span className="text-4xl shrink-0">🏆</span>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<h3 className="font-bold text-amber-900 dark:text-amber-100 text-lg">
|
|
||||||
Selamat! Tim Anda meraih JUARA {winnerEntry.rank}
|
|
||||||
</h3>
|
|
||||||
<p className="text-amber-700 dark:text-amber-300 text-sm">
|
|
||||||
Anda dapat generate sertifikat penghargaan dan
|
|
||||||
membagikannya.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
const team = myTeams[0] as { id?: string } | null | undefined;
|
|
||||||
if (!team?.id) return;
|
|
||||||
const certId = await encodeWinnerCertificateId(team.id);
|
|
||||||
navigate(`/certificate/winner/${encodeURIComponent(certId)}`);
|
|
||||||
}}
|
|
||||||
className="shrink-0 px-6 py-2 bg-amber-600 hover:bg-amber-700 dark:bg-amber-600 dark:hover:bg-amber-700 text-white font-medium rounded-lg transition-colors cursor-pointer"
|
|
||||||
>
|
|
||||||
Generate Sertifikat Juara
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Countdown Timer */}
|
{/* Countdown Timer */}
|
||||||
{timeLeft && !isSubmissionDeadlinePassed && (
|
{timeLeft && !isSubmissionDeadlinePassed && (
|
||||||
<div className="mb-8 bg-blue-50 dark:bg-blue-900/20 border-2 border-blue-500 rounded-lg p-6">
|
<div className="mb-8 bg-blue-50 dark:bg-blue-900/20 border-2 border-blue-500 rounded-lg p-6">
|
||||||
@@ -236,8 +194,7 @@ const DashboardPage: FC = (): ReactElement => {
|
|||||||
<div className="mb-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-3">
|
<div className="mb-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-lg p-3">
|
||||||
<p className="text-sm text-amber-700 dark:text-amber-300 flex items-center gap-2">
|
<p className="text-sm text-amber-700 dark:text-amber-300 flex items-center gap-2">
|
||||||
<Icon icon="mdi:clock-alert" className="text-lg shrink-0" />
|
<Icon icon="mdi:clock-alert" className="text-lg shrink-0" />
|
||||||
Team features are closed. You can no longer accept
|
Team features are closed. You can no longer accept invitations.
|
||||||
invitations.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -306,10 +263,7 @@ const DashboardPage: FC = (): ReactElement => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="w-16 h-16 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center shrink-0">
|
<div className="w-16 h-16 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center shrink-0">
|
||||||
<Icon
|
<Icon icon="mdi:account-group" className="text-gray-500 dark:text-gray-400 text-2xl" />
|
||||||
icon="mdi:account-group"
|
|
||||||
className="text-gray-500 dark:text-gray-400 text-2xl"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex-1 min-w-0">
|
<div className="flex-1 min-w-0">
|
||||||
@@ -319,33 +273,19 @@ const DashboardPage: FC = (): ReactElement => {
|
|||||||
<div className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-3 font-sans mt-2">
|
<div className="text-sm text-gray-600 dark:text-gray-400 flex items-center gap-3 font-sans mt-2">
|
||||||
{team.has_submission && (
|
{team.has_submission && (
|
||||||
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 shrink-0">
|
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 shrink-0">
|
||||||
<Icon
|
<Icon icon="mdi:check-circle" className="text-sm" />
|
||||||
icon="mdi:check-circle"
|
|
||||||
className="text-sm"
|
|
||||||
/>
|
|
||||||
Submitted
|
Submitted
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{team.city && (
|
{team.city && (
|
||||||
<span className="flex items-center gap-1 truncate">
|
<span className="flex items-center gap-1 truncate">
|
||||||
<Icon
|
<Icon icon="mdi:map-marker" className="shrink-0" />
|
||||||
icon="mdi:map-marker"
|
|
||||||
className="shrink-0"
|
|
||||||
/>
|
|
||||||
<span className="truncate">{team.city}</span>
|
<span className="truncate">{team.city}</span>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className="flex items-center gap-1 shrink-0">
|
<span className="flex items-center gap-1 shrink-0">
|
||||||
<Icon icon="mdi:account-group" />
|
<Icon icon="mdi:account-group" />
|
||||||
{team.member_count ||
|
{team.member_count || team.members?.length || 0} member{(team.member_count || team.members?.length || 0) !== 1 ? 's' : ''}
|
||||||
team.members?.length ||
|
|
||||||
0}{' '}
|
|
||||||
member
|
|
||||||
{(team.member_count ||
|
|
||||||
team.members?.length ||
|
|
||||||
0) !== 1
|
|
||||||
? 's'
|
|
||||||
: ''}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -52,12 +52,6 @@ export default function RootLayout() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Certificate page - allow public access
|
|
||||||
if (pathname.startsWith('/certificate/')) {
|
|
||||||
setIsChecking(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Require authentication for all other routes
|
// Require authentication for all other routes
|
||||||
if (!session) {
|
if (!session) {
|
||||||
navigate('/auth/login', { replace: true });
|
navigate('/auth/login', { replace: true });
|
||||||
|
|||||||
@@ -554,7 +554,7 @@ export default function HomePage() {
|
|||||||
{/* Judge 2 */}
|
{/* Judge 2 */}
|
||||||
<div className="flex flex-col justify-between bg-white dark:bg-gray-800 rounded-xl px-4 py-8 shadow-lg text-center">
|
<div className="flex flex-col justify-between bg-white dark:bg-gray-800 rounded-xl px-4 py-8 shadow-lg text-center">
|
||||||
<h3 className="text-p3 font-bold mb-1 dark:text-white">
|
<h3 className="text-p3 font-bold mb-1 dark:text-white">
|
||||||
Muhammad Alif Ramadhan
|
Anka Tama
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
<p className="text-primary-500 font-semibold mb-1">Admin</p>
|
<p className="text-primary-500 font-semibold mb-1">Admin</p>
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
import { FC, ReactElement } from 'react';
|
import { FC, ReactElement } from 'react';
|
||||||
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
import { Button } from '@imphnen-frontend-service/ui/atoms';
|
||||||
import { useNavigate, useParams } from 'react-router';
|
import { useNavigate, useParams } from 'react-router';
|
||||||
import { useTeamById, useTeamSubmission, useAuthStore } from '@imphnen-frontend-service/service';
|
import { useTeamById, useTeamSubmission } from '@imphnen-frontend-service/service';
|
||||||
import { encodeCertificateId } from '../../../../utils/certificate';
|
import { encodeCertificateId } from '../../../../utils/certificate';
|
||||||
|
|
||||||
const SubmissionViewPage: FC = (): ReactElement => {
|
const SubmissionViewPage: FC = (): ReactElement => {
|
||||||
const { teamId } = useParams<{ teamId: string }>();
|
const { teamId } = useParams<{ teamId: string }>();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { session } = useAuthStore();
|
|
||||||
|
|
||||||
const { data: teamData } = useTeamById(teamId || '');
|
const { data: teamData } = useTeamById(teamId || '');
|
||||||
const { data: submissionData, isLoading } = useTeamSubmission(teamId || '', !!teamId);
|
const { data: submissionData, isLoading } = useTeamSubmission(teamId || '', !!teamId);
|
||||||
@@ -114,17 +113,13 @@ const SubmissionViewPage: FC = (): ReactElement => {
|
|||||||
View Your Certificate
|
View Your Certificate
|
||||||
</h3>
|
</h3>
|
||||||
<p className="text-amber-700 dark:text-amber-300 text-sm">
|
<p className="text-amber-700 dark:text-amber-300 text-sm">
|
||||||
Congratulations! Your personalized certificate is ready to download and share.
|
Congratulations! Your certificate is ready to download and share.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const certId = await encodeCertificateId(
|
const certId = await encodeCertificateId(teamId || '', submission.id);
|
||||||
teamId || '',
|
|
||||||
submission.id,
|
|
||||||
session?.user?.id || ''
|
|
||||||
);
|
|
||||||
navigate(`/certificate/${encodeURIComponent(certId)}`);
|
navigate(`/certificate/${encodeURIComponent(certId)}`);
|
||||||
}}
|
}}
|
||||||
className="shrink-0 px-6 py-2 bg-amber-600 hover:bg-amber-700 dark:bg-amber-600 dark:hover:bg-amber-700 text-white font-medium rounded-lg transition-colors"
|
className="shrink-0 px-6 py-2 bg-amber-600 hover:bg-amber-700 dark:bg-amber-600 dark:hover:bg-amber-700 text-white font-medium rounded-lg transition-colors"
|
||||||
|
|||||||
@@ -3,69 +3,27 @@ import { decryptText, encryptText } from "./aesclient";
|
|||||||
const SECRET_KEY = 'imphnen-hackathon-2025';
|
const SECRET_KEY = 'imphnen-hackathon-2025';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode teamId, submissionId, and userId into a certificate ID
|
* Encode teamId and submissionId into a certificate ID
|
||||||
* Uses AES encryption for secure encoding
|
* Uses base64 encoding for simple obfuscation
|
||||||
* @param teamId - The team ID
|
* @param teamId - The team ID
|
||||||
* @param submissionId - The submission ID
|
* @param submissionId - The submission ID
|
||||||
* @param userId - The user ID (team member)
|
|
||||||
* @returns Encoded certificate ID
|
* @returns Encoded certificate ID
|
||||||
*/
|
*/
|
||||||
export const encodeCertificateId = async (teamId: string, submissionId: string, userId: string): Promise<string> => {
|
export const encodeCertificateId = async (teamId: string, submissionId: string): Promise<string> => {
|
||||||
const combined = `${teamId}::${submissionId}::${userId}`;
|
const combined = `${teamId}::${submissionId}`;
|
||||||
return encryptText(combined, SECRET_KEY);
|
return encryptText(combined, SECRET_KEY);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encode winner certificate ID (team-based)
|
* Decode certificate ID back to teamId and submissionId
|
||||||
* @param teamId - Winner team ID
|
|
||||||
* @returns Encoded winner certificate ID
|
|
||||||
*/
|
|
||||||
export const encodeWinnerCertificateId = async (teamId: string): Promise<string> => {
|
|
||||||
const combined = `winner::${teamId}`;
|
|
||||||
return encryptText(combined, SECRET_KEY);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode certificate ID back to teamId, submissionId, and userId
|
|
||||||
* @param certId - The encoded certificate ID
|
* @param certId - The encoded certificate ID
|
||||||
* @returns Object containing teamId, submissionId, and userId
|
* @returns Object containing teamId and submissionId
|
||||||
*/
|
*/
|
||||||
export const decodeCertificateId = async (certId: string): Promise<{ teamId: string; submissionId: string; userId: string }> => {
|
export const decodeCertificateId = async (certId: string): Promise<{ teamId: string; submissionId: string }> => {
|
||||||
try {
|
try {
|
||||||
const decoded = await decryptText(certId, SECRET_KEY);
|
const decoded = await decryptText(certId, SECRET_KEY);
|
||||||
const parts = decoded.split('::');
|
const [teamId, submissionId] = decoded.split('::');
|
||||||
|
return { teamId, submissionId };
|
||||||
// Handle both old format (teamId::submissionId) and new format (teamId::submissionId::userId)
|
|
||||||
if (parts.length === 2) {
|
|
||||||
const [teamId, submissionId] = parts;
|
|
||||||
return { teamId, submissionId, userId: '' };
|
|
||||||
} else if (parts.length === 3) {
|
|
||||||
const [teamId, submissionId, userId] = parts;
|
|
||||||
return { teamId, submissionId, userId };
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Invalid certificate format');
|
|
||||||
} catch {
|
|
||||||
throw new Error('Invalid certificate ID');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Decode winner certificate ID back to teamId
|
|
||||||
* @param certId - The encoded winner certificate ID
|
|
||||||
* @returns Object containing teamId
|
|
||||||
*/
|
|
||||||
export const decodeWinnerCertificateId = async (certId: string): Promise<{ teamId: string }> => {
|
|
||||||
try {
|
|
||||||
const decoded = await decryptText(certId, SECRET_KEY);
|
|
||||||
const parts = decoded.split('::');
|
|
||||||
|
|
||||||
// winner::teamId
|
|
||||||
if (parts.length === 2 && parts[0] === 'winner') {
|
|
||||||
return { teamId: parts[1] };
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Invalid winner certificate format');
|
|
||||||
} catch {
|
} catch {
|
||||||
throw new Error('Invalid certificate ID');
|
throw new Error('Invalid certificate ID');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import baseConfig from '../../eslint.config.mjs';
|
|
||||||
|
|
||||||
export default [...baseConfig];
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<title>IMPHNEN Infrastructure</title>
|
|
||||||
<base href="/" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<link rel="icon" type="image/x-icon" href="/logos/simple.svg" />
|
|
||||||
<link rel="stylesheet" href="/src/index.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="root"></div>
|
|
||||||
<script type="module" src="/src/main.tsx"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
plugins: {
|
|
||||||
'@tailwindcss/postcss': {
|
|
||||||
base: join(import.meta.dirname, '../../'),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "infra",
|
|
||||||
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
||||||
"sourceRoot": "apps/infra/src",
|
|
||||||
"projectType": "application",
|
|
||||||
"tags": [],
|
|
||||||
"targets": {}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
<svg width="317" height="170" viewBox="0 0 317 170" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<rect width="317" height="169.613" fill="url(#pattern0_164_317)"/>
|
|
||||||
<defs>
|
|
||||||
<pattern id="pattern0_164_317" patternContentUnits="objectBoundingBox" width="1" height="1">
|
|
||||||
<use xlink:href="#image0_164_317" transform="scale(0.00184502 0.00344828)"/>
|
|
||||||
</pattern>
|
|
||||||
<image id="image0_164_317" width="542" height="290" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAh4AAAEiCAYAAAC2k9sBAAAABGdBTUEAALGPC/xhBQAACklpQ0NQc1JHQiBJRUM2MTk2Ni0yLjEAAEiJnVN3WJP3Fj7f92UPVkLY8LGXbIEAIiOsCMgQWaIQkgBhhBASQMWFiApWFBURnEhVxILVCkidiOKgKLhnQYqIWotVXDjuH9yntX167+3t+9f7vOec5/zOec8PgBESJpHmomoAOVKFPDrYH49PSMTJvYACFUjgBCAQ5svCZwXFAADwA3l4fnSwP/wBr28AAgBw1S4kEsfh/4O6UCZXACCRAOAiEucLAZBSAMguVMgUAMgYALBTs2QKAJQAAGx5fEIiAKoNAOz0ST4FANipk9wXANiiHKLine truncated
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 351 KiB |
@@ -1,9 +0,0 @@
|
|||||||
<svg width="138" height="52" viewBox="0 0 138 52" fill="none" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
|
||||||
<rect width="137.505" height="51.7538" fill="url(#pattern0_164_316)"/>
|
|
||||||
<defs>
|
|
||||||
<pattern id="pattern0_164_316" patternContentUnits="objectBoundingBox" width="1" height="1">
|
|
||||||
<use xlink:href="#image0_164_316" transform="matrix(0.000440723 0 0 0.00117096 -0.173204 -1.3185)"/>
|
|
||||||
</pattern>
|
|
||||||
<image id="image0_164_316" width="3000" height="3000" preserveAspectRatio="none" xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAC7gAAAu4CAYAAABxFfV5AAAACXBIWXMAAC4jAAAuIwF4pT92AAX6AElEQVR4nOzaMQEAIAzAsIF/z0MGPRIFFdCzuwMAAAAAAAAAAAAAAL/d3wEAAAAAAAAAAAAAADBjcAcAAAAAAAAAAAAAIMLgDgAAAAAAAAAAAABAgsEdAAAAAAAAAAAAAIAEgzsAAAAAAAAAAAAAAAkGdwAAAAAAAAAAAAAAEgzuAAAAAAAAAAAAAAAkGNwBAAAAAAAAAAAAAEgwuAMAAAAAAAAAAAAAkGBwBwAAAAAAAAAAAAAgweAOAAAAAAAAAAAAAECCwR0AAAAAAAAAAAAAgASDOwAAAAAAAAAAAAAACQZ3AAAALine truncated
|
|
||||||
</defs>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 511 KiB |
@@ -1,248 +0,0 @@
|
|||||||
const apps = [
|
|
||||||
{ name: 'Landing', domain: 'imphnen.dev', type: 'Next.js', color: 'blue' },
|
|
||||||
{ name: 'WWW', domain: 'www.imphnen.dev', type: 'Redirect', color: 'purple' },
|
|
||||||
{ name: 'Gacha', domain: 'gacha.imphnen.dev', type: 'Vite', color: 'green' },
|
|
||||||
{ name: 'Backoffice', domain: 'backoffice.imphnen.dev', type: 'Vite', color: 'green' },
|
|
||||||
{ name: 'Dimentorin', domain: 'dimentorin.imphnen.dev', type: 'Vite', color: 'green' },
|
|
||||||
{ name: 'Hackathon', domain: 'hackathon.imphnen.dev', type: 'Vite', color: 'green' },
|
|
||||||
{ name: 'QR Campaign', domain: 'qr.imphnen.dev', type: 'Vite', color: 'green' },
|
|
||||||
{ name: 'Infra', domain: 'infra.imphnen.dev', type: 'Vite', color: 'green' },
|
|
||||||
{ name: 'API QR', domain: 'api-qr.imphnen.dev', type: 'Backend', color: 'orange' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const techStack = [
|
|
||||||
{ category: 'OS', items: ['NixOS 25.05'] },
|
|
||||||
{ category: 'Web Server', items: ['Nginx'] },
|
|
||||||
{ category: 'SSL', items: ['Let\'s Encrypt (ACME)'] },
|
|
||||||
{ category: 'Build', items: ['Nix Flakes', 'Nx Monorepo'] },
|
|
||||||
{ category: 'CI/CD', items: ['GitHub Actions', 'Cachix'] },
|
|
||||||
{ category: 'Frontend', items: ['React', 'Next.js', 'Vite', 'TailwindCSS'] },
|
|
||||||
{ category: 'Backend', items: ['Go', 'Supabase'] },
|
|
||||||
{ category: 'Secrets', items: ['sops-nix'] },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function App() {
|
|
||||||
return (
|
|
||||||
<div className="min-h-screen bg-[#0a0a0a]">
|
|
||||||
{/* Header */}
|
|
||||||
<header className="border-b border-zinc-800 bg-[#0a0a0a]/80 backdrop-blur-sm sticky top-0 z-50">
|
|
||||||
<div className="container mx-auto px-6 py-4">
|
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<div className="w-8 h-8 bg-blue-500 rounded-lg flex items-center justify-center font-bold">
|
|
||||||
I
|
|
||||||
</div>
|
|
||||||
<span className="text-xl font-semibold">IMPHNEN Infrastructure</span>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href="https://github.com/IMPHNEN"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="text-zinc-400 hover:text-white transition-colors"
|
|
||||||
>
|
|
||||||
GitHub
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<main className="container mx-auto px-6 py-12">
|
|
||||||
{/* Hero */}
|
|
||||||
<section className="text-center mb-16">
|
|
||||||
<h1 className="text-4xl md:text-5xl font-bold mb-4">
|
|
||||||
Infrastructure Overview
|
|
||||||
</h1>
|
|
||||||
<p className="text-zinc-400 text-lg max-w-2xl mx-auto">
|
|
||||||
Declarative NixOS infrastructure powering IMPHNEN's frontend applications
|
|
||||||
on Hetzner Cloud.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Architecture Diagram */}
|
|
||||||
<section className="mb-16">
|
|
||||||
<h2 className="text-2xl font-semibold mb-6">Architecture</h2>
|
|
||||||
<div className="card bg-[#141414] border-zinc-800 p-8">
|
|
||||||
<div className="flex flex-col items-center gap-6">
|
|
||||||
{/* Internet */}
|
|
||||||
<div className="flex items-center gap-2 text-zinc-400">
|
|
||||||
<span className="text-2xl">🌐</span>
|
|
||||||
<span>Internet</span>
|
|
||||||
</div>
|
|
||||||
<div className="w-px h-8 bg-zinc-700" />
|
|
||||||
|
|
||||||
{/* Cloudflare */}
|
|
||||||
<div className="card bg-orange-500/10 border-orange-500/30 px-6 py-3">
|
|
||||||
<span className="badge badge-orange">Cloudflare</span>
|
|
||||||
<p className="text-sm text-zinc-400 mt-1">DNS + CDN + DDoS Protection</p>
|
|
||||||
</div>
|
|
||||||
<div className="w-px h-8 bg-zinc-700" />
|
|
||||||
|
|
||||||
{/* Hetzner VPS */}
|
|
||||||
<div className="card bg-[#1a1a1a] border-zinc-700 w-full max-w-3xl">
|
|
||||||
<div className="text-center mb-4">
|
|
||||||
<span className="badge badge-purple">Hetzner Cloud VPS</span>
|
|
||||||
<p className="text-sm text-zinc-400 mt-1">NixOS | 167.235.70.37</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Nginx */}
|
|
||||||
<div className="card bg-green-500/10 border-green-500/30 mb-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<span className="badge badge-green">Nginx</span>
|
|
||||||
<p className="text-sm text-zinc-400 mt-1">Reverse Proxy + Static Files + SSL Termination</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Apps Grid */}
|
|
||||||
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
|
|
||||||
{apps.map((app) => (
|
|
||||||
<div
|
|
||||||
key={app.name}
|
|
||||||
className="card bg-[#0a0a0a] border-zinc-800 text-center p-3"
|
|
||||||
>
|
|
||||||
<p className="font-medium text-sm">{app.name}</p>
|
|
||||||
<p className="text-xs text-zinc-500">{app.domain}</p>
|
|
||||||
<span className={`badge badge-${app.color} mt-2 text-xs`}>
|
|
||||||
{app.type}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Deployed Apps */}
|
|
||||||
<section className="mb-16">
|
|
||||||
<h2 className="text-2xl font-semibold mb-6">Deployed Applications</h2>
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{apps.map((app) => (
|
|
||||||
<a
|
|
||||||
key={app.name}
|
|
||||||
href={`https://${app.domain}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="card bg-[#141414] border-zinc-800 hover:border-zinc-600 transition-colors group"
|
|
||||||
>
|
|
||||||
<div className="flex items-start justify-between">
|
|
||||||
<div>
|
|
||||||
<h3 className="font-semibold group-hover:text-blue-400 transition-colors">
|
|
||||||
{app.name}
|
|
||||||
</h3>
|
|
||||||
<p className="text-sm text-zinc-500">{app.domain}</p>
|
|
||||||
</div>
|
|
||||||
<span className={`badge badge-${app.color}`}>{app.type}</span>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Tech Stack */}
|
|
||||||
<section className="mb-16">
|
|
||||||
<h2 className="text-2xl font-semibold mb-6">Technology Stack</h2>
|
|
||||||
<div className="grid md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
||||||
{techStack.map((tech) => (
|
|
||||||
<div key={tech.category} className="card bg-[#141414] border-zinc-800">
|
|
||||||
<h3 className="text-sm text-zinc-500 uppercase tracking-wider mb-2">
|
|
||||||
{tech.category}
|
|
||||||
</h3>
|
|
||||||
<div className="flex flex-wrap gap-2">
|
|
||||||
{tech.items.map((item) => (
|
|
||||||
<span key={item} className="badge badge-blue">
|
|
||||||
{item}
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Deployment Flow */}
|
|
||||||
<section className="mb-16">
|
|
||||||
<h2 className="text-2xl font-semibold mb-6">Deployment Flow</h2>
|
|
||||||
<div className="card bg-[#141414] border-zinc-800">
|
|
||||||
<div className="flex flex-col md:flex-row items-center justify-between gap-4">
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="w-12 h-12 bg-zinc-800 rounded-full flex items-center justify-center mx-auto mb-2">
|
|
||||||
<span>1</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium">Push to GitHub</p>
|
|
||||||
<p className="text-xs text-zinc-500">develop branch</p>
|
|
||||||
</div>
|
|
||||||
<div className="hidden md:block text-zinc-600">→</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="w-12 h-12 bg-zinc-800 rounded-full flex items-center justify-center mx-auto mb-2">
|
|
||||||
<span>2</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium">GitHub Actions</p>
|
|
||||||
<p className="text-xs text-zinc-500">nix build</p>
|
|
||||||
</div>
|
|
||||||
<div className="hidden md:block text-zinc-600">→</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="w-12 h-12 bg-orange-500 rounded-full flex items-center justify-center mx-auto mb-2">
|
|
||||||
<span>3</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium">Push to Cachix</p>
|
|
||||||
<p className="text-xs text-zinc-500">binary cache</p>
|
|
||||||
</div>
|
|
||||||
<div className="hidden md:block text-zinc-600">→</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="w-12 h-12 bg-zinc-800 rounded-full flex items-center justify-center mx-auto mb-2">
|
|
||||||
<span>4</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium">Server Pull</p>
|
|
||||||
<p className="text-xs text-zinc-500">nixos-rebuild</p>
|
|
||||||
</div>
|
|
||||||
<div className="hidden md:block text-zinc-600">→</div>
|
|
||||||
<div className="text-center">
|
|
||||||
<div className="w-12 h-12 bg-blue-500 rounded-full flex items-center justify-center mx-auto mb-2">
|
|
||||||
<span>✓</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium">Live</p>
|
|
||||||
<p className="text-xs text-zinc-500">Zero downtime</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
{/* Repositories */}
|
|
||||||
<section>
|
|
||||||
<h2 className="text-2xl font-semibold mb-6">Repositories</h2>
|
|
||||||
<div className="grid md:grid-cols-2 gap-4">
|
|
||||||
<a
|
|
||||||
href="https://github.com/IMPHNEN/imphnen-frontend-service"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="card bg-[#141414] border-zinc-800 hover:border-zinc-600 transition-colors"
|
|
||||||
>
|
|
||||||
<h3 className="font-semibold mb-1">imphnen-frontend-service</h3>
|
|
||||||
<p className="text-sm text-zinc-400">
|
|
||||||
Nx monorepo containing all frontend applications
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href="https://github.com/IMPHNEN/imphnen-infrastructure"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
className="card bg-[#141414] border-zinc-800 hover:border-zinc-600 transition-colors"
|
|
||||||
>
|
|
||||||
<h3 className="font-semibold mb-1">imphnen-infrastructure</h3>
|
|
||||||
<p className="text-sm text-zinc-400">
|
|
||||||
NixOS flake configuration for server deployment
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{/* Footer */}
|
|
||||||
<footer className="border-t border-zinc-800 mt-16">
|
|
||||||
<div className="container mx-auto px-6 py-8 text-center text-zinc-500 text-sm">
|
|
||||||
<p>IMPHNEN Infrastructure © {new Date().getFullYear()}</p>
|
|
||||||
</div>
|
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
@import 'tailwindcss';
|
|
||||||
|
|
||||||
@theme {
|
|
||||||
--color-primary: #3b82f6;
|
|
||||||
--color-primary-hover: #2563eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer base {
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
||||||
background-color: #0a0a0a;
|
|
||||||
color: #ffffff;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@layer components {
|
|
||||||
.card {
|
|
||||||
background: #141414;
|
|
||||||
border: 1px solid #27272a;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 0.25rem 0.75rem;
|
|
||||||
border-radius: 9999px;
|
|
||||||
font-size: 0.75rem;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-blue {
|
|
||||||
background: rgba(59, 130, 246, 0.2);
|
|
||||||
color: #60a5fa;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-green {
|
|
||||||
background: rgba(34, 197, 94, 0.2);
|
|
||||||
color: #4ade80;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-purple {
|
|
||||||
background: rgba(168, 85, 247, 0.2);
|
|
||||||
color: #c084fc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge-orange {
|
|
||||||
background: rgba(249, 115, 22, 0.2);
|
|
||||||
color: #fb923c;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import { createRoot } from 'react-dom/client';
|
|
||||||
import { StrictMode } from 'react';
|
|
||||||
import App from './app/App';
|
|
||||||
import './index.css';
|
|
||||||
|
|
||||||
const rootElement = document.getElementById('root');
|
|
||||||
|
|
||||||
if (!rootElement) throw new Error('Failed to find the root element');
|
|
||||||
|
|
||||||
createRoot(rootElement).render(
|
|
||||||
<StrictMode>
|
|
||||||
<App />
|
|
||||||
</StrictMode>
|
|
||||||
);
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "./tsconfig.json",
|
|
||||||
"compilerOptions": {
|
|
||||||
"outDir": "../../dist/out-tsc",
|
|
||||||
"types": ["node", "vite/client"]
|
|
||||||
},
|
|
||||||
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts", "src/**/*.spec.tsx", "src/**/*.test.tsx"],
|
|
||||||
"include": ["src/**/*.js", "src/**/*.jsx", "src/**/*.ts", "src/**/*.tsx"]
|
|
||||||
}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"jsx": "react-jsx",
|
|
||||||
"allowJs": false,
|
|
||||||
"esModuleInterop": false,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"strict": true,
|
|
||||||
"types": ["vite/client"]
|
|
||||||
},
|
|
||||||
"files": [],
|
|
||||||
"include": [],
|
|
||||||
"references": [
|
|
||||||
{
|
|
||||||
"path": "./tsconfig.app.json"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"extends": "../../tsconfig.base.json"
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
/// <reference types='vitest' />
|
|
||||||
import { defineConfig } from 'vite';
|
|
||||||
import react from '@vitejs/plugin-react';
|
|
||||||
import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
|
|
||||||
import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
|
|
||||||
|
|
||||||
export default defineConfig(() => ({
|
|
||||||
root: __dirname,
|
|
||||||
cacheDir: '../../node_modules/.vite/apps/infra',
|
|
||||||
server: {
|
|
||||||
port: 3006,
|
|
||||||
host: 'localhost',
|
|
||||||
},
|
|
||||||
preview: {
|
|
||||||
port: 3007,
|
|
||||||
host: 'localhost',
|
|
||||||
},
|
|
||||||
plugins: [react(), nxViteTsPaths(), nxCopyAssetsPlugin(['*.md'])],
|
|
||||||
build: {
|
|
||||||
outDir: '../../dist/apps/infra',
|
|
||||||
emptyOutDir: true,
|
|
||||||
reportCompressedSize: true,
|
|
||||||
commonjsOptions: {
|
|
||||||
transformMixedEsModules: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -47,7 +47,7 @@ export function CommunitySection() {
|
|||||||
case 'FaDiscord':
|
case 'FaDiscord':
|
||||||
return FaDiscord;
|
return FaDiscord;
|
||||||
case 'FaGithub':
|
case 'FaGithub':
|
||||||
return FaGithub;
|
return FaDiscord;
|
||||||
case 'FaInstagram':
|
case 'FaInstagram':
|
||||||
return FaInstagram;
|
return FaInstagram;
|
||||||
case 'FaTiktok':
|
case 'FaTiktok':
|
||||||
|
|||||||
Loaded 50 of 111 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user