feat: initialize smirk-website with connect flow
- Next.js 16 with TypeScript and Tailwind CSS - Single-asset authentication (user picks favorite coin to sign) - Logo with glow animation - Coin picker UI with all 5 assets - API client for backend auth endpoints - TypeScript types for window.smirk extension API
@@ -0,0 +1,41 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
@@ -0,0 +1,130 @@
|
||||
# Smirk Website
|
||||
|
||||
User dashboard for Smirk wallet users. Requires the Smirk browser extension to access.
|
||||
|
||||
## Overview
|
||||
|
||||
This website serves as a hub for Smirk wallet users to:
|
||||
- View their account UUID and balances
|
||||
- See tipping history (sent and received)
|
||||
- Manage connected accounts (Telegram, Discord, etc.)
|
||||
- Future: Chat/messaging between users, direct tipping, etc.
|
||||
|
||||
**Important**: This site is ONLY accessible to users with the Smirk extension installed. Authentication happens via cryptographic signatures from the user's wallet keys.
|
||||
|
||||
## Architecture
|
||||
|
||||
### Deployment Options
|
||||
|
||||
The website is designed to be deployable either:
|
||||
1. **Alongside the backend** - Served from the same server as smirk-backend (current plan)
|
||||
2. **Separately** - As a standalone deployment (e.g., Vercel, separate server)
|
||||
|
||||
Keep this flexibility in mind when developing - avoid tight coupling to backend internals.
|
||||
|
||||
### Extension Communication
|
||||
|
||||
The Smirk extension injects a `window.smirk` API object into web pages. The website uses this to:
|
||||
|
||||
1. **Detect extension** - Check if `window.smirk` exists
|
||||
2. **Connect** - Request user's public keys (with user approval)
|
||||
3. **Sign** - Request signatures for authentication challenges (BTC/LTC: ECDSA, XMR/WOW/Grin: Ed25519)
|
||||
|
||||
```
|
||||
┌─────────────┐ window.smirk ┌─────────────────┐
|
||||
│ Website │ ◄──────────────────► │ Smirk Extension │
|
||||
└──────┬──────┘ └────────┬────────┘
|
||||
│ │
|
||||
│ POST /auth/verify │ User approves
|
||||
│ { publicKeys, signatures } │ in popup
|
||||
▼ ▼
|
||||
┌─────────────┐ ┌─────────────────┐
|
||||
│ Backend │ │ Extension UI │
|
||||
└─────────────┘ └─────────────────┘
|
||||
```
|
||||
|
||||
### Authentication Flow
|
||||
|
||||
1. User clicks "Connect with Smirk"
|
||||
2. Website calls `window.smirk.connect()`
|
||||
3. Extension shows approval popup
|
||||
4. User approves → Extension returns public keys for all 5 assets
|
||||
5. Website calls `POST /api/v1/auth/website/challenge` with origin
|
||||
6. Backend returns challenge message (with nonce + timestamp + origin)
|
||||
7. Website calls `window.smirk.signMessage(challenge)`
|
||||
8. Extension shows signing popup with the message
|
||||
9. User approves → Extension returns signatures from all 5 keys
|
||||
10. Website sends `{ challenge_id, signatures }` to `POST /api/v1/auth/website/verify`
|
||||
11. Backend verifies signatures, looks up user by BTC pubkey_hash, issues JWT session
|
||||
12. Website stores JWT, user is now authenticated
|
||||
|
||||
**Future**: Allow single-asset auth (user picks their favorite coin to sign with).
|
||||
|
||||
### Tech Stack
|
||||
|
||||
- **Framework**: Next.js 14+ (App Router)
|
||||
- **Styling**: Tailwind CSS
|
||||
- **State**: React Context / Zustand (TBD)
|
||||
- **Auth**: JWT sessions verified against backend
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
# Install dependencies
|
||||
npm install
|
||||
|
||||
# Run development server
|
||||
npm run dev
|
||||
|
||||
# Build for production
|
||||
npm run build
|
||||
|
||||
# Start production server
|
||||
npm start
|
||||
```
|
||||
|
||||
## Environment Variables
|
||||
|
||||
```bash
|
||||
# Backend API URL
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3000
|
||||
|
||||
# For production
|
||||
NEXT_PUBLIC_API_URL=https://api.smirk.cash
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
|
||||
```
|
||||
src/
|
||||
├── app/ # Next.js App Router pages
|
||||
│ ├── page.tsx # Landing / Connect page
|
||||
│ ├── dashboard/ # Protected dashboard routes
|
||||
│ │ ├── page.tsx # Main dashboard (balances, UUID)
|
||||
│ │ ├── history/ # Tip history
|
||||
│ │ └── settings/ # Account settings
|
||||
│ └── layout.tsx # Root layout
|
||||
├── components/ # React components
|
||||
│ ├── ConnectButton.tsx # "Connect with Smirk" button
|
||||
│ ├── Dashboard/ # Dashboard components
|
||||
│ └── ui/ # Shared UI components
|
||||
├── lib/
|
||||
│ ├── smirk.ts # window.smirk API wrapper
|
||||
│ ├── auth.ts # Authentication utilities
|
||||
│ └── api.ts # Backend API client
|
||||
└── types/
|
||||
└── smirk.d.ts # TypeScript types for window.smirk
|
||||
```
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- Challenge messages include timestamps to prevent replay attacks
|
||||
- Signatures are verified server-side against registered public keys
|
||||
- JWT tokens have reasonable expiration
|
||||
- HTTPS required in production
|
||||
- CSP headers to prevent XSS
|
||||
|
||||
## Related Projects
|
||||
|
||||
- [smirk-extension](../smirk-extension) - Browser extension (injects `window.smirk`)
|
||||
- [smirk-backend](../smirk-backend) - Rust backend API
|
||||
@@ -0,0 +1,147 @@
|
||||
# Smirk Website TODO
|
||||
|
||||
## Phase 1: Foundation (Critical Path)
|
||||
|
||||
### Extension API (DONE - 2026-01-22)
|
||||
- [x] **Inject `window.smirk` API** - Content script injects API object into pages
|
||||
- [x] **`window.smirk.connect()`** - Returns public keys for all 5 assets
|
||||
- [x] **`window.smirk.signMessage(message)`** - Signs message with all 5 asset keys
|
||||
- [x] **`window.smirk.isConnected()`** - Check if site is connected
|
||||
- [x] **`window.smirk.disconnect()`** - Revoke site access
|
||||
- [x] **`window.smirk.getPublicKeys()`** - Get public keys if already connected
|
||||
- [x] **Approval popup UI** - Show user what site is requesting, allow approve/deny
|
||||
- [x] **Origin allowlist** - Track which origins user has approved (persisted to storage)
|
||||
|
||||
### Backend Auth Endpoints (DONE - 2026-01-24)
|
||||
- [x] `POST /api/v1/auth/website/challenge` - Generate challenge with nonce + origin
|
||||
- [x] `POST /api/v1/auth/website/verify` - Verify signatures, issue JWT session token
|
||||
- [x] `GET /api/v1/auth/me` - Get current user info from JWT
|
||||
- [ ] JWT middleware for protected routes (frontend side)
|
||||
|
||||
### Website Basics
|
||||
- [ ] Initialize Next.js project with App Router
|
||||
- [ ] Tailwind CSS setup
|
||||
- [ ] Basic layout (header, footer)
|
||||
- [ ] Landing page with "Connect with Smirk" button
|
||||
- [ ] Extension detection (`window.smirk` check)
|
||||
- [ ] "Install Smirk" fallback if extension not detected
|
||||
|
||||
## Phase 2: Core Dashboard
|
||||
|
||||
### Authentication Flow
|
||||
- [ ] Connect button triggers `window.smirk.connect()`
|
||||
- [ ] Challenge generation and signing
|
||||
- [ ] Backend verification and JWT storage
|
||||
- [ ] Protected route middleware
|
||||
- [ ] Logout functionality
|
||||
|
||||
### Dashboard - Basic
|
||||
- [ ] Display user UUID
|
||||
- [ ] Display balances for all 5 assets
|
||||
- [ ] Auto-refresh balances
|
||||
- [ ] Loading states and error handling
|
||||
|
||||
### Dashboard - Tip History
|
||||
- [ ] List of tips sent
|
||||
- [ ] List of tips received
|
||||
- [ ] Pagination
|
||||
- [ ] Filter by asset type
|
||||
- [ ] Click to view tip details
|
||||
|
||||
## Phase 3: Account Management
|
||||
|
||||
### Connected Accounts
|
||||
- [ ] Link Telegram account
|
||||
- [ ] Link Discord account
|
||||
- [ ] Link other platforms (Twitter/X, etc.)
|
||||
- [ ] Manage/unlink accounts
|
||||
- [ ] Display linked accounts on dashboard
|
||||
|
||||
### Settings
|
||||
- [ ] Notification preferences
|
||||
- [ ] Privacy settings
|
||||
- [ ] Export account data
|
||||
|
||||
## Phase 4: Social Features (Future)
|
||||
|
||||
### Direct Tipping
|
||||
- [ ] Send tip to another Smirk user by UUID
|
||||
- [ ] Send tip via username/handle
|
||||
- [ ] Tip confirmation flow
|
||||
- [ ] Tip notifications
|
||||
|
||||
### Messaging (Future)
|
||||
- [ ] Direct messages between users
|
||||
- [ ] Encrypted messaging (E2E?)
|
||||
- [ ] Message notifications
|
||||
|
||||
### Discovery (Future)
|
||||
- [ ] Public profiles (opt-in)
|
||||
- [ ] Search users
|
||||
- [ ] Leaderboards?
|
||||
|
||||
## Technical Debt / Polish
|
||||
|
||||
- [ ] Comprehensive error handling
|
||||
- [ ] Loading skeletons
|
||||
- [ ] Mobile responsive design
|
||||
- [ ] Dark/light theme (sync with extension setting?)
|
||||
- [ ] Accessibility (a11y)
|
||||
- [ ] SEO optimization
|
||||
- [ ] Analytics integration
|
||||
- [ ] Rate limiting on frontend
|
||||
- [ ] E2E tests with Playwright
|
||||
|
||||
## Deployment
|
||||
|
||||
- [ ] Docker configuration
|
||||
- [ ] Nginx reverse proxy setup (alongside backend)
|
||||
- [ ] Environment configuration
|
||||
- [ ] CI/CD pipeline
|
||||
- [ ] Monitoring and alerting
|
||||
|
||||
---
|
||||
|
||||
## Notes
|
||||
|
||||
### Extension Work Required (add to smirk-extension/TODO.md)
|
||||
|
||||
The website cannot function without these extension features:
|
||||
|
||||
1. **Content script injection** - Must inject `window.smirk` on all pages (or configurable origins)
|
||||
2. **Message passing** - Content script ↔ Background script communication
|
||||
3. **Approval UI** - Popup for connect/sign requests
|
||||
4. **Security** - Origin tracking, user approval persistence
|
||||
|
||||
### Signing Algorithm Notes
|
||||
|
||||
For message signing (all implemented in extension):
|
||||
- **BTC/LTC**: ECDSA signature (secp256k1) with Bitcoin message signing format
|
||||
- **XMR/WOW**: Ed25519 signature using private spend key
|
||||
- **Grin**: Ed25519 signature using slatepack key
|
||||
|
||||
Backend must implement verification for all signature types.
|
||||
|
||||
### Challenge Message Format
|
||||
|
||||
```
|
||||
Sign this message to authenticate with smirk.cash
|
||||
|
||||
Timestamp: 2026-01-22T12:00:00Z
|
||||
Nonce: abc123...
|
||||
Origin: https://smirk.cash
|
||||
```
|
||||
|
||||
Challenge should expire after ~5 minutes to prevent replay attacks.
|
||||
|
||||
### Single-Asset Authentication (Future Enhancement)
|
||||
|
||||
**Current implementation**: Backend requires signatures from all 5 asset keys.
|
||||
|
||||
**Better approach**: Let users pick their favorite coin to sign with. Benefits:
|
||||
1. **Simpler UX** - User chooses one coin, signs once
|
||||
2. **User data** - "What's your favorite coin?" gives us insight into our user base
|
||||
3. **Proof of concept** - Demonstrates wallet-based auth for ANY of these 5 coins
|
||||
4. **Still secure** - We already know all 5 pubkeys; signing with ANY one proves wallet ownership
|
||||
|
||||
This positions Smirk as infrastructure for wallet-based authentication across multiple cryptocurrencies. Other websites could use the same pattern - "Sign in with Bitcoin/Litecoin/Monero/Wownero/Grin".
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "smirk-website",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"next": "16.1.4",
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.1.4",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,7 @@
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="64" width="64" version="1.1" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/">
|
||||
<g transform="translate(0.00630876,-0.00301984)">
|
||||
<path fill="#f7931a" d="m63.033,39.744c-4.274,17.143-21.637,27.576-38.782,23.301-17.138-4.274-27.571-21.638-23.295-38.78,4.272-17.145,21.635-27.579,38.775-23.305,17.144,4.274,27.576,21.64,23.302,38.784z"/>
|
||||
<path fill="#FFF" d="m46.103,27.444c0.637-4.258-2.605-6.547-7.038-8.074l1.438-5.768-3.511-0.875-1.4,5.616c-0.923-0.23-1.871-0.447-2.813-0.662l1.41-5.653-3.509-0.875-1.439,5.766c-0.764-0.174-1.514-0.346-2.242-0.527l0.004-0.018-4.842-1.209-0.934,3.75s2.605,0.597,2.55,0.634c1.422,0.355,1.679,1.296,1.636,2.042l-1.638,6.571c0.098,0.025,0.225,0.061,0.365,0.117-0.117-0.029-0.242-0.061-0.371-0.092l-2.296,9.205c-0.174,0.432-0.615,1.08-1.609,0.834,0.035,0.051-2.552-0.637-2.552-0.637l-1.743,4.019,4.569,1.139c0.85,0.213,1.683,0.436,2.503,0.646l-1.453,5.834,3.507,0.875,1.439-5.772c0.958,0.26,1.888,0.5,2.798,0.726l-1.434,5.745,3.511,0.875,1.453-5.823c5.987,1.133,10.489,0.676,12.384-4.739,1.527-4.36-0.076-6.875-3.226-8.515,2.294-0.529,4.022-2.038,4.483-5.155zm-8.022,11.249c-1.085,4.36-8.426,2.003-10.806,1.412l1.928-7.729c2.38,0.594,10.012,1.77,8.878,6.317zm1.086-11.312c-0.99,3.966-7.1,1.951-9.082,1.457l1.748-7.01c1.982,0.494,8.365,1.416,7.334,5.553z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>␍
|
||||
<!-- Generator: Adobe Illustrator 19.2.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->␍
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"␍
|
||||
viewBox="0 0 191 191" style="enable-background:new 0 0 191 191;" xml:space="preserve">␍
|
||||
<style type="text/css">␍
|
||||
.st0{fill:#FEF102;}␍
|
||||
</style>␍
|
||||
<circle cx="95.5" cy="95.5" r="95.5"/>␍
|
||||
<circle class="st0" cx="95.3" cy="95.5" r="85.3"/>␍
|
||||
<path d="M135.7,63.5c-2-4.6-3.9-13.6-9.1-15.6c-6.7-2.6-10.9,14.3-11.9,18.6h-1c-1.7-7.3-4-17.7-12-20c-3.7,15.6,3.9,32.5,11,46␍
|
||||
c7.6-3.6,11.3-15,12-23h1l8,24c6.4-1.8,8.7-8.2,11-14c5-12.4,9.2-27.7,6-41C141.1,41.2,138.2,55,135.7,63.5 M39.7,95.5␍
|
||||
c8.6-4.1,12.7-17.1,14-26h1c1,4.3,3.7,15.4,9.1,16.1c6.8,0.9,10.4-14.3,10.9-19.1h1c2.2,7.5,4.5,17.3,12,21␍
|
||||
c2.8-11.7-0.5-24.1-4.8-35c-1.1-2.8-3.3-10.4-7.2-10.4c-6.3,0-9.9,16.7-11,21.4h-1l-8-24C41.6,46.2,34,82,39.7,95.5 M24.7,107.5␍
|
||||
c10.4,45.6,58.6,71.4,102,52.1c15.8-7,28.3-19.5,35.5-35.1c1.9-4,5.5-11.4,3.4-16.7c-2.1-5.3-22.8,3.4-27.9,5.6␍
|
||||
c-0.7,0.3-1.2,1-1.4,1.8c-0.3,2.2,1.2,4.3,3.4,4.6c0.1,0,0.2,0,0.3,0l8.7,0.7c-15.7,30.4-58.1,43.8-88,19.5␍
|
||||
C54,134.6,48,128.2,44,120.5c-1.9-3.7-3.7-8.4-7-11C33.8,107.1,29,106.3,24.7,107.5L24.7,107.5z"/>␍
|
||||
</svg>␍
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 82.6 82.6"><title>litecoin-ltc-logo</title><circle cx="41.3" cy="41.3" r="36.83" style="fill:#fff"/><path d="M41.3,0A41.3,41.3,0,1,0,82.6,41.3h0A41.18,41.18,0,0,0,41.54,0ZM42,42.7,37.7,57.2h23a1.16,1.16,0,0,1,1.2,1.12v.38l-2,6.9a1.49,1.49,0,0,1-1.5,1.1H23.2l5.9-20.1-6.6,2L24,44l6.6-2,8.3-28.2a1.51,1.51,0,0,1,1.5-1.1h8.9a1.16,1.16,0,0,1,1.2,1.12v.38L43.5,38l6.6-2-1.4,4.8Z" style="fill:#345d9d"/></svg>
|
||||
|
After Width: | Height: | Size: 489 B |
@@ -0,0 +1 @@
|
||||
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 3756.09 3756.49"><title>monero</title><path d="M4128,2249.81C4128,3287,3287.26,4127.86,2250,4127.86S372,3287,372,2249.81,1212.76,371.75,2250,371.75,4128,1212.54,4128,2249.81Z" transform="translate(-371.96 -371.75)" style="fill:#fff"/><path id="_149931032" data-name=" 149931032" d="M2250,371.75c-1036.89,0-1879.12,842.06-1877.8,1878,0.26,207.26,33.31,406.63,95.34,593.12h561.88V1263L2250,2483.57,3470.52,1263v1579.9h562c62.12-186.48,95-385.85,95.37-593.12C4129.66,1212.76,3287,372,2250,372Z" transform="translate(-371.96 -371.75)" style="fill:#f26822"/><path id="_149931160" data-name=" 149931160" d="M1969.3,2764.17l-532.67-532.7v994.14H1029.38l-384.29.07c329.63,540.8,925.35,902.56,1604.91,902.56S3525.31,3766.4,3855,3225.6H3063.25V2231.47l-532.7,532.7-280.61,280.61-280.62-280.61h0Z" transform="translate(-371.96 -371.75)" style="fill:#4d4d4d"/></svg>
|
||||
|
After Width: | Height: | Size: 940 B |
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 26.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="svg898" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 64 64" style="enable-background:new 0 0 64 64;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#FF2AD4;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
.st2{fill:#FFCC00;}
|
||||
</style>
|
||||
<path id="path6" class="st0" d="M61.5,19.5C54.6,3.3,35.8-4.3,19.5,2.5s-23.9,25.7-17,41.9s25.7,23.9,41.9,17
|
||||
c7.7-3.2,13.8-9.3,17-17C64.8,36.5,64.8,27.5,61.5,19.5z"/>
|
||||
<path class="st1" d="M7.4,26.3h8.2v15.6v6.4l4.6-4.4l11.9-11.2l11.8,11.2l4.6,4.4v-6.4l0-15.6h8.1c0.4,1.8,0.6,3.7,0.6,5.6
|
||||
c0,13.9-11.3,25.3-25.2,25.3S6.7,45.8,6.7,31.9C6.7,30,6.9,28.1,7.4,26.3 M5.3,23.5C0.7,38.3,8.9,54,23.7,58.6s30.5-3.7,35-18.4
|
||||
c0.8-2.7,1.3-5.5,1.3-8.3c0-2.8-0.4-5.6-1.3-8.4H45.8l-0.1,18.4L32.1,28.8L18.3,41.9V23.5H5.3z"/>
|
||||
<path class="st2" d="M18.3,20.8H21v2.8v12l9.1-8.7l1.9-1.8l1.9,1.8l9.1,8.6v-12v-2.7h14.7C51.5,6.6,34.9,0.2,20.8,6.4
|
||||
c-6.4,2.8-11.6,8-14.4,14.4H18.3z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="164.43007mm"
|
||||
height="142.10118mm"
|
||||
viewBox="0 0 164.43007 142.10118"
|
||||
version="1.1"
|
||||
id="svg5"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
|
||||
sodipodi:docname="logo_w_text.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview7"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
inkscape:zoom="0.64693624"
|
||||
inkscape:cx="70.331506"
|
||||
inkscape:cy="386.43685"
|
||||
inkscape:window-width="2124"
|
||||
inkscape:window-height="1236"
|
||||
inkscape:window-x="5342"
|
||||
inkscape:window-y="533"
|
||||
inkscape:window-maximized="0"
|
||||
inkscape:current-layer="layer1" /><defs
|
||||
id="defs2" /><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(-20.986536,-70.520522)"><circle
|
||||
style="fill:#000000;fill-opacity:1;stroke-width:1.80275"
|
||||
id="path464-3"
|
||||
cx="102.98067"
|
||||
cy="138.87482"
|
||||
r="43.459656" /><circle
|
||||
style="fill:#fbeb0a;fill-opacity:1;stroke-width:1.64883"
|
||||
id="path464"
|
||||
cx="102.98067"
|
||||
cy="138.87482"
|
||||
r="39.748943" /><path
|
||||
style="fill:#000000;stroke-width:0.0417246"
|
||||
d="m 120.01899,137.4727 c -0.0945,-0.29259 -0.2164,-0.65403 -0.27088,-0.80319 -0.0545,-0.14917 -0.19792,-0.58102 -0.31877,-0.95967 -0.12086,-0.37865 -0.27093,-0.82928 -0.33351,-1.00139 -0.0626,-0.17211 -0.23747,-0.68846 -0.38866,-1.14743 -0.15119,-0.45897 -0.29479,-0.89082 -0.3191,-0.95966 -0.0243,-0.0689 -0.42743,-1.27827 -0.89581,-2.68762 -0.46838,-1.40934 -0.87592,-2.57787 -0.90564,-2.59672 -0.0626,-0.0397 -0.22419,-0.0447 -0.35045,-0.0108 -0.0786,0.0211 -0.0905,0.0636 -0.1133,0.40624 -0.0677,1.01625 -0.60455,3.35207 -0.98049,4.26614 -0.0465,0.11305 -0.13455,0.33698 -0.19567,0.49762 -0.0611,0.16065 -0.14237,0.36439 -0.18056,0.45278 -0.12503,0.28937 -0.72141,1.46505 -0.77567,1.52914 -0.0292,0.0344 -0.0638,0.0932 -0.077,0.13058 -0.0828,0.23492 -0.95323,1.34099 -1.38649,1.76186 -0.59149,0.57457 -1.54112,1.23781 -1.69761,1.18564 -0.0605,-0.0202 -0.34,-0.54095 -0.91305,-1.70116 -0.25503,-0.51635 -0.54936,-1.10779 -0.65407,-1.31433 -0.2116,-0.41736 -0.75251,-1.59963 -1.04039,-2.27399 -1.10804,-2.59566 -1.93449,-5.12296 -2.2854,-6.98887 -0.27842,-1.48039 -0.29453,-1.58262 -0.35511,-2.25313 -0.0249,-0.27538 -0.0636,-0.69784 -0.086,-0.9388 -0.0773,-0.83042 -0.0321,-2.58144 0.0888,-3.44228 0.064,-0.45545 0.14032,-0.89444 0.2258,-1.29843 l 0.0717,-0.33875 0.2768,0.0994 c 1.40822,0.50567 2.52323,1.6172 3.38422,3.37366 0.34701,0.70791 0.4934,1.07202 0.73814,1.83588 0.10294,0.32128 0.22151,0.68741 0.2635,0.81363 0.0922,0.27713 0.4122,1.52379 0.46592,1.81502 0.0211,0.11474 0.0546,0.24618 0.0744,0.29207 0.0335,0.0778 0.11267,0.43593 0.1384,0.62587 0.009,0.0674 0.0473,0.0855 0.19869,0.0943 0.10306,0.006 0.21112,-0.005 0.24014,-0.0237 0.029,-0.019 0.15573,-0.44523 0.28159,-0.94716 0.25536,-1.01838 0.53763,-1.97468 0.68961,-2.33626 0.0193,-0.0459 0.0813,-0.22427 0.13779,-0.39638 0.10254,-0.31235 0.21499,-0.60507 0.50491,-1.31432 0.19027,-0.46547 0.72955,-1.50338 0.94929,-1.827 0.90699,-1.33582 1.89503,-1.84911 2.81309,-1.46141 0.56161,0.23718 1.0452,0.62619 1.5287,1.22974 0.40354,0.50373 0.9589,1.55483 1.33022,2.51763 0.0443,0.11475 0.0961,0.24618 0.11529,0.29208 0.11971,0.28683 0.21289,0.5529 0.21289,0.6079 0,0.0351 0.017,0.0817 0.0379,0.1035 0.0208,0.0218 0.0569,0.10535 0.0802,0.18568 0.0233,0.0803 0.0925,0.27746 0.15369,0.4381 0.0612,0.16064 0.12746,0.33902 0.14718,0.39639 0.0197,0.0574 0.0851,0.23574 0.1452,0.39638 0.0601,0.16064 0.1473,0.40004 0.19368,0.53199 0.0464,0.13195 0.0975,0.23991 0.11373,0.23991 0.0263,0 0.16332,-0.44975 0.25411,-0.83449 0.19513,-0.82686 0.88265,-3.07602 1.24992,-4.08901 0.7226,-1.99306 1.54671,-3.45449 2.59206,-4.5966 0.57405,-0.62719 1.58121,-1.30472 2.23989,-1.5068 0.17324,-0.0531 0.30229,-0.11599 0.30251,-0.14731 4.2e-4,-0.03 0.0177,-0.0439 0.0387,-0.0309 0.0966,0.0597 0.32141,1.32134 0.44654,2.50557 0.0996,0.94246 0.0993,3.15009 -4.6e-4,4.15159 -0.13983,1.40308 -0.33341,2.6784 -0.56684,3.73435 -0.1224,0.55371 -0.45045,1.89447 -0.52578,2.14882 -0.0306,0.10326 -0.093,0.31919 -0.13869,0.47983 -0.14939,0.52507 -0.58236,1.90751 -0.77101,2.46175 -0.22786,0.66949 -1.08777,2.98214 -1.19461,3.21279 -0.0212,0.0459 -0.0745,0.17733 -0.11837,0.29207 -0.13031,0.34107 -0.38022,0.94611 -0.47394,1.14743 -0.50114,1.07653 -0.95016,1.92046 -1.18055,2.21885 -0.0459,0.0594 -0.11161,0.15105 -0.14603,0.20357 -0.20336,0.31029 -0.68405,0.83229 -1.07364,1.1659 -0.19363,0.16581 -0.68606,0.4799 -1.03346,0.65918 -0.18569,0.0958 -0.6564,0.25876 -0.74755,0.25876 -0.0385,0 -0.11808,-0.18366 -0.23058,-0.53199 z"
|
||||
id="path2693" /><path
|
||||
style="fill:#000000;stroke-width:0.0417246"
|
||||
d="m 87.065159,138.61502 c -1.362193,-0.15528 -2.017187,-0.29489 -3.150208,-0.67145 -0.857456,-0.28498 -1.32444,-0.48763 -2.018161,-0.87579 -0.468539,-0.26217 -1.218073,-0.75071 -1.465844,-0.95543 -1.239235,-1.02392 -1.606164,-1.41976 -2.28127,-2.461 -0.332413,-0.5127 -0.855826,-1.49505 -1.098983,-2.00434 -0.48597,-1.01786 -2.260387,-3.28883 0.252915,-3.08358 1.064567,0.0869 0.790069,0.0665 1.643984,0.11631 1.169025,-0.041 2.079099,0.47579 3.256652,1.42374 0.63205,0.50882 0.777018,0.6279 1.022253,0.83974 0.137691,0.11895 0.363004,0.29902 0.500695,0.40016 0.137691,0.10114 0.259736,0.19462 0.27121,0.20774 0.01147,0.0131 0.12413,0.091 0.250348,0.17319 1.069322,0.69591 2.222996,1.08669 3.609178,1.22253 0.312633,0.0306 1.226151,-0.0407 1.668984,-0.13037 1.182633,-0.23943 2.096969,-0.82627 2.466782,-1.58326 0.15691,-0.32119 0.161715,-0.34412 0.156859,-0.74858 -0.01386,-1.15472 -0.627503,-1.9424 -2.026692,-2.6015 -0.716785,-0.33765 -1.352605,-0.52703 -2.808353,-0.83648 -1.060418,-0.22541 -1.204318,-0.25811 -1.857519,-0.42203 -1.732248,-0.43473 -2.892563,-1.03611 -3.880705,-2.01132 -1.123982,-1.10927 -1.75776,-2.53076 -1.823072,-4.08891 -0.04504,-1.0746 0.135353,-1.90695 0.631073,-2.9118 0.308714,-0.62578 0.672205,-1.1293 1.201924,-1.66495 1.000331,-1.01152 1.932007,-1.598 3.287409,-2.06935 1.980139,-0.68861 4.14237,-0.78864 6.175242,-0.28567 0.814303,0.20147 1.325837,0.38346 2.029618,0.72209 1.442947,0.69427 1.770424,1.06037 2.479495,2.24093 0.31689,0.5276 0.789865,1.23468 1.248716,2.53013 0.158242,0.44675 0.769587,0.87729 -0.23814,0.90412 -0.394823,0.0105 -0.871505,0.11809 -1.510723,0.11552 -0.646307,-0.003 -0.893613,0.1645 -1.785234,-0.24348 -2.601623,-1.19041 -3.225024,-1.36986 -4.581172,-1.31877 -0.741168,0.0279 -1.303652,0.15049 -1.83643,0.40016 -1.116747,0.52332 -1.53891,1.18206 -1.167581,1.82188 0.185348,0.31936 0.668601,0.69356 1.208552,0.93582 0.365349,0.16392 1.191748,0.42723 1.816322,0.57873 2.09589,0.50838 2.461512,0.61539 3.44228,1.00753 0.506742,0.20261 1.428738,0.67537 1.930047,0.98964 0.911031,0.57115 1.921841,1.47923 2.463002,2.2127 0.213745,0.2897 0.676491,1.05061 0.676491,1.11238 0,0.0103 0.05452,0.13733 0.121148,0.28236 0.715941,1.55831 0.80087,3.48618 0.22943,5.20806 -0.133998,0.40376 -0.554861,1.31854 -0.66018,1.43494 -0.02478,0.0274 -0.06383,0.08 -0.08678,0.11698 -0.367256,0.59123 -1.054236,1.36436 -1.543811,1.73742 -0.137691,0.10493 -0.259736,0.20167 -0.27121,0.215 -0.06236,0.0724 -0.6204,0.4392 -1.004052,0.6599 -1.256396,0.72279 -2.520835,1.11622 -4.252003,1.32299 -0.46963,0.0561 -2.29979,0.0801 -2.692483,0.0354 z"
|
||||
id="path2691"
|
||||
sodipodi:nodetypes="ssssssscsssssssssssssssssssssssssssssssssssssssssss" /><path
|
||||
style="fill:#000000;stroke-width:0.0417246"
|
||||
d="m 93.337178,169.65921 c -0.76646,-0.088 -1.50511,-0.24803 -2.312166,-0.50108 -0.748203,-0.2346 -0.863846,-0.28043 -1.923052,-0.76218 -0.529418,-0.24079 -1.57368,-0.82832 -1.877607,-1.05639 -0.05737,-0.0431 -0.282684,-0.20296 -0.500695,-0.35534 -0.218011,-0.15239 -0.405772,-0.28855 -0.417246,-0.30258 -0.01147,-0.014 -0.226627,-0.19373 -0.478119,-0.39933 -1.62797,-1.33092 -2.940886,-2.96468 -3.938462,-4.90094 -0.764824,-1.4845 -1.197619,-2.79186 -1.443434,-4.36022 -0.154371,-0.98493 -0.195184,-1.2107 -0.223083,-1.23408 -0.01625,-0.0137 -0.273646,-0.11439 -0.571977,-0.22393 -2.253126,-0.82732 -4.099378,-1.69381 -5.148308,-2.41622 -0.441374,-0.30398 -0.930166,-0.87019 -1.157932,-1.34135 -0.209542,-0.43346 -0.265448,-0.68312 -0.265227,-1.18442 3.76e-4,-0.85924 0.208275,-1.48514 0.652751,-1.96523 0.300709,-0.3248 0.624748,-0.48669 1.090073,-0.54459 0.674127,-0.0839 1.314707,0.0678 2.387754,0.56542 2.172633,1.00755 3.318277,1.4807 3.585237,1.4807 0.09936,0 0.119918,-0.0355 0.272166,-0.46941 0.185132,-0.52766 0.745508,-1.66447 1.023244,-2.07579 0.948991,-1.40545 2.057782,-2.39489 3.464274,-3.0914 0.875099,-0.43334 1.82355,-0.72712 2.941585,-0.91112 2.19197,-0.36076 4.370677,-0.0914 5.549372,0.68615 0.734998,0.48483 1.018948,1.08214 0.968904,2.03813 -0.03901,0.74524 -0.251871,1.29117 -0.705292,1.8089 -0.32672,0.37305 -0.538205,0.53403 -1.016169,0.77348 -0.682047,0.34169 -1.343228,0.53304 -2.418512,0.6999 -1.652239,0.2564 -3.038894,0.92409 -3.683524,1.77366 -0.34145,0.45001 -0.450453,0.64615 -0.382446,0.68818 0.10613,0.0656 2.812496,0.70531 3.857775,0.91188 2.367522,0.46789 5.199956,0.84323 7.885522,1.04497 1.515236,0.11382 2.906296,0.18024 2.944566,0.14058 0.0177,-0.0184 0.12537,-0.23995 0.23918,-0.49238 0.11381,-0.25244 0.34059,-0.75415 0.50395,-1.11493 0.16336,-0.36077 0.29702,-0.6658 0.29702,-0.67784 0,-0.012 0.21171,-0.44912 0.47047,-0.9713 0.96757,-1.95259 1.83302,-3.19165 3.15775,-4.52092 1.61576,-1.6213 3.28473,-2.59743 5.65551,-3.30772 1.83795,-0.55065 3.88891,-0.6959 5.88316,-0.41663 1.4763,0.20673 2.95533,0.74276 4.20608,1.52437 1.32952,0.83082 2.60006,2.13702 3.53002,3.6291 0.26177,0.41999 0.83052,1.53098 1.07005,2.09022 0.0663,0.15491 0.14385,0.28164 0.17223,0.28164 0.0284,0 0.23543,-0.0668 0.46012,-0.14854 1.0933,-0.39756 1.5125,-0.53306 2.70337,-0.8738 0.97705,-0.27957 1.60849,-0.39565 2.17624,-0.40008 0.55975,-0.004 0.80323,0.0804 1.06566,0.37083 0.22032,0.24388 0.26332,0.41051 0.26057,1.00987 -0.005,1.12244 -0.38777,2.17846 -1.12091,3.09358 -0.99457,1.24145 -2.4285,2.19232 -4.26512,2.82831 l -0.63183,0.21879 -0.0292,0.27806 c -0.15655,1.49271 -0.77176,3.49852 -1.51221,4.93035 -0.78144,1.5111 -1.5622,2.56765 -2.8058,3.79694 -1.07045,1.05813 -2.53858,2.07773 -3.91251,2.71721 -0.24687,0.11491 -0.47948,0.22524 -0.51692,0.24519 -0.20796,0.11086 -1.07752,0.44526 -1.67037,0.64235 -0.99801,0.33179 -1.24198,0.37908 -1.95419,0.37878 -0.59384,-4.2e-4 -0.61104,-0.003 -0.93208,-0.14058 -0.57697,-0.24755 -0.93556,-0.70721 -1.14261,-1.46465 -0.10966,-0.40119 -0.0984,-1.36526 0.0209,-1.80147 0.2779,-1.01523 0.88293,-1.89256 1.68055,-2.43693 0.17083,-0.11658 0.56407,-0.33061 0.87388,-0.4756 2.40279,-1.12459 3.60325,-1.9321 4.52963,-3.04692 0.3547,-0.42687 0.5135,-0.66697 0.87016,-1.31569 0.251,-0.45652 0.35239,-0.70666 0.30546,-0.75359 -0.0104,-0.0104 -0.2516,0.0372 -0.53595,0.10582 -1.11164,0.26828 -2.69812,0.56485 -4.68947,0.87664 -1.01966,0.15964 -2.47916,0.33106 -4.79833,0.56356 -1.01716,0.10197 -2.86083,0.23278 -3.72711,0.26445 l -0.80639,0.0295 -0.11708,0.29207 c -0.50443,1.25832 -1.44463,3.0481 -2.16993,4.13074 -0.9131,1.36294 -1.47667,2.03663 -2.40063,2.86968 -1.17483,1.05923 -2.311402,1.73636 -3.650919,2.17509 -1.230834,0.40313 -2.305805,0.56954 -3.63004,0.56196 -0.424548,-0.003 -0.997142,-0.0303 -1.272431,-0.0619 z m 1.564503,-6.61275 c 1.305332,-0.25767 2.306514,-0.83931 3.213691,-1.86701 0.24896,-0.28204 1.042219,-1.42974 1.042219,-1.50791 0,-0.0187 -0.154903,-0.0466 -0.344228,-0.0621 -2.69734,-0.22101 -5.826446,-0.6005 -7.437412,-0.90201 -2.216672,-0.41487 -2.779443,-0.53404 -4.040843,-0.85563 -0.40953,-0.10441 -0.75555,-0.17888 -0.768934,-0.1655 -0.07478,0.0748 0.516777,1.41235 0.888168,2.00822 0.585611,0.93958 1.451126,1.76358 2.461248,2.3432 0.902853,0.51807 2.31243,0.95625 3.383458,1.05179 0.45564,0.0406 1.292576,0.0182 1.602633,-0.043 z m 14.791389,-9.11392 c 2.29082,-0.15389 5.41631,-0.56373 7.63966,-1.0018 0.75055,-0.14789 3.44432,-0.7627 3.47467,-0.79304 0.0285,-0.0285 -0.24099,-0.51764 -0.44738,-0.81196 -0.27456,-0.39156 -0.78801,-0.87828 -1.2532,-1.18796 -0.58018,-0.38625 -1.0986,-0.60205 -1.92419,-0.80097 -0.37291,-0.0899 -0.56847,-0.10873 -1.31432,-0.12688 -0.75931,-0.0185 -0.94302,-0.01 -1.37691,0.0645 -0.87255,0.14962 -1.42508,0.3315 -2.14853,0.70725 -1.06181,0.55149 -1.8515,1.26563 -2.54758,2.30383 -0.27136,0.40473 -0.79556,1.39751 -0.85601,1.62118 -0.0258,0.0956 -0.0209,0.0977 0.17584,0.0747 0.11134,-0.013 0.37141,-0.035 0.57795,-0.0489 z"
|
||||
id="path302" /><text
|
||||
xml:space="preserve"
|
||||
style="font-size:32.4556px;line-height:31px;font-family:'Baekmuk Headline';-inkscape-font-specification:'Baekmuk Headline';letter-spacing:-1.10596px;fill:#000000;stroke-width:3.166"
|
||||
x="18.909378"
|
||||
y="210.4949"
|
||||
id="text238"><tspan
|
||||
sodipodi:role="line"
|
||||
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:32.4556px;font-family:'Syne Mono';-inkscape-font-specification:'Syne Mono Bold Italic';stroke-width:3.166"
|
||||
x="18.909378"
|
||||
y="210.4949"
|
||||
id="tspan244">smirk.ca$h</tspan></text><text
|
||||
xml:space="preserve"
|
||||
style="font-style:italic;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:25.4px;font-family:'Syne Mono';-inkscape-font-specification:'Syne Mono Bold Italic';fill:#000000;stroke-width:3.166"
|
||||
x="19.559481"
|
||||
y="88.300522"
|
||||
id="text408"><tspan
|
||||
sodipodi:role="line"
|
||||
id="tspan406"
|
||||
style="font-size:25.4px;stroke-width:3.166"
|
||||
x="19.559481"
|
||||
y="88.300522">smirk wallet</tspan></text></g></svg>
|
||||
|
After Width: | Height: | Size: 14 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,69 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: #0a0a0a;
|
||||
--foreground: #ededed;
|
||||
--smirk-yellow: #fbeb0a;
|
||||
--smirk-yellow-dark: #d4c708;
|
||||
}
|
||||
|
||||
@theme inline {
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-smirk: var(--smirk-yellow);
|
||||
--font-mono: var(--font-geist-mono);
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-mono), monospace;
|
||||
}
|
||||
|
||||
/* Logo animation */
|
||||
@keyframes pulse-glow {
|
||||
0%, 100% {
|
||||
filter: drop-shadow(0 0 8px rgba(251, 235, 10, 0.4));
|
||||
}
|
||||
50% {
|
||||
filter: drop-shadow(0 0 20px rgba(251, 235, 10, 0.8));
|
||||
}
|
||||
}
|
||||
|
||||
.logo-glow {
|
||||
animation: pulse-glow 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Coin button styles */
|
||||
.coin-btn {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 1rem;
|
||||
border-radius: 0.75rem;
|
||||
border: 2px solid #3f3f46;
|
||||
transition: all 0.2s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.coin-btn:hover {
|
||||
border-color: #fbeb0a;
|
||||
background: rgba(39, 39, 42, 0.5);
|
||||
}
|
||||
|
||||
.coin-btn.selected {
|
||||
border-color: #fbeb0a;
|
||||
background: rgba(39, 39, 42, 0.5);
|
||||
}
|
||||
|
||||
.coin-btn img {
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.coin-btn span {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type { Metadata } from "next";
|
||||
import { Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Smirk Wallet",
|
||||
description: "Sign in with your Smirk wallet",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${geistMono.variable} font-mono antialiased`}>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import Image from 'next/image';
|
||||
import { getChallenge, verifySignature, type AuthResponse } from '@/lib/api';
|
||||
|
||||
type Asset = 'btc' | 'ltc' | 'xmr' | 'wow' | 'grin';
|
||||
|
||||
const COINS: { id: Asset; name: string; icon: string }[] = [
|
||||
{ id: 'btc', name: 'Bitcoin', icon: '/coins/bitcoin.svg' },
|
||||
{ id: 'ltc', name: 'Litecoin', icon: '/coins/litecoin.svg' },
|
||||
{ id: 'xmr', name: 'Monero', icon: '/coins/monero.svg' },
|
||||
{ id: 'wow', name: 'Wownero', icon: '/coins/wownero.svg' },
|
||||
{ id: 'grin', name: 'Grin', icon: '/coins/grin.svg' },
|
||||
];
|
||||
|
||||
type Step = 'initial' | 'choose-coin' | 'signing' | 'logged-in';
|
||||
|
||||
export default function Home() {
|
||||
const [step, setStep] = useState<Step>('initial');
|
||||
const [selectedCoin, setSelectedCoin] = useState<Asset | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [user, setUser] = useState<AuthResponse['user'] | null>(null);
|
||||
const [hasExtension, setHasExtension] = useState<boolean | null>(null);
|
||||
|
||||
// Check for extension on mount
|
||||
useEffect(() => {
|
||||
const check = () => setHasExtension(!!window.smirk);
|
||||
// Check immediately and after a delay (extension might inject late)
|
||||
check();
|
||||
const timeout = setTimeout(check, 500);
|
||||
return () => clearTimeout(timeout);
|
||||
}, []);
|
||||
|
||||
const handleConnect = async () => {
|
||||
if (!window.smirk) {
|
||||
setError('Smirk extension not found');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setError(null);
|
||||
// Connect to extension (get public keys)
|
||||
await window.smirk.connect();
|
||||
setStep('choose-coin');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to connect');
|
||||
}
|
||||
};
|
||||
|
||||
const handleCoinSelect = async (coin: Asset) => {
|
||||
if (!window.smirk) return;
|
||||
|
||||
setSelectedCoin(coin);
|
||||
setStep('signing');
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Get challenge from backend
|
||||
const challenge = await getChallenge(window.location.origin);
|
||||
|
||||
// Sign with the selected coin
|
||||
const signatures = await window.smirk.signMessage(challenge.challenge);
|
||||
const sig = signatures[coin];
|
||||
|
||||
// Verify with backend
|
||||
const auth = await verifySignature(challenge.challenge_id, {
|
||||
asset: coin,
|
||||
signature: sig.signature,
|
||||
public_key: sig.public_key,
|
||||
});
|
||||
|
||||
// Store token and show logged in
|
||||
localStorage.setItem('smirk_token', auth.access_token);
|
||||
localStorage.setItem('smirk_refresh', auth.refresh_token);
|
||||
setUser(auth.user);
|
||||
setStep('logged-in');
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Authentication failed');
|
||||
setStep('choose-coin');
|
||||
}
|
||||
};
|
||||
|
||||
const handleLogout = () => {
|
||||
localStorage.removeItem('smirk_token');
|
||||
localStorage.removeItem('smirk_refresh');
|
||||
setUser(null);
|
||||
setSelectedCoin(null);
|
||||
setStep('initial');
|
||||
};
|
||||
|
||||
// Check for existing session on mount
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('smirk_token');
|
||||
if (token) {
|
||||
// TODO: Validate token with /auth/me
|
||||
// For now, just clear it and require re-auth
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col items-center justify-center p-8">
|
||||
{/* Logo */}
|
||||
<div className="mb-12 logo-glow">
|
||||
<Image
|
||||
src="/logo_w_text.svg"
|
||||
alt="Smirk Wallet"
|
||||
width={200}
|
||||
height={180}
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Initial state - Connect button */}
|
||||
{step === 'initial' && (
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
{hasExtension === false && (
|
||||
<p className="text-zinc-400 text-sm mb-4">
|
||||
Install the{' '}
|
||||
<a
|
||||
href="https://chrome.google.com/webstore/detail/smirk-wallet"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-[#fbeb0a] hover:underline"
|
||||
>
|
||||
Smirk extension
|
||||
</a>{' '}
|
||||
to continue
|
||||
</p>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleConnect}
|
||||
disabled={hasExtension === false}
|
||||
className="px-8 py-4 bg-[#fbeb0a] text-black font-bold rounded-xl
|
||||
hover:bg-[#d4c708] transition-colors disabled:opacity-50
|
||||
disabled:cursor-not-allowed text-lg"
|
||||
>
|
||||
Connect with Smirk
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Choose coin step */}
|
||||
{step === 'choose-coin' && (
|
||||
<div className="flex flex-col items-center gap-6">
|
||||
<h2 className="text-xl font-semibold mb-2">Choose your coin to sign in</h2>
|
||||
<p className="text-zinc-400 text-sm mb-4">
|
||||
Pick your favorite cryptocurrency to authenticate
|
||||
</p>
|
||||
|
||||
<div className="grid grid-cols-5 gap-4">
|
||||
{COINS.map((coin) => (
|
||||
<button
|
||||
key={coin.id}
|
||||
onClick={() => handleCoinSelect(coin.id)}
|
||||
className={`coin-btn ${selectedCoin === coin.id ? 'selected' : ''}`}
|
||||
>
|
||||
<Image src={coin.icon} alt={coin.name} width={48} height={48} />
|
||||
<span>{coin.id}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Signing step */}
|
||||
{step === 'signing' && (
|
||||
<div className="flex flex-col items-center gap-4">
|
||||
<div className="animate-spin w-8 h-8 border-2 border-[#fbeb0a] border-t-transparent rounded-full" />
|
||||
<p className="text-zinc-400">
|
||||
Signing with {selectedCoin?.toUpperCase()}...
|
||||
</p>
|
||||
<p className="text-zinc-500 text-sm">Check your extension</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Logged in state */}
|
||||
{step === 'logged-in' && user && (
|
||||
<div className="flex flex-col items-center gap-6 text-center">
|
||||
<div className="w-16 h-16 bg-[#fbeb0a] rounded-full flex items-center justify-center">
|
||||
<svg
|
||||
className="w-8 h-8 text-black"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={3}
|
||||
d="M5 13l4 4L19 7"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold mb-2">Logged in!</h2>
|
||||
<p className="text-zinc-400 text-sm">
|
||||
Signed in with {selectedCoin?.toUpperCase()}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="bg-zinc-900 rounded-xl p-6 w-full max-w-md">
|
||||
<p className="text-zinc-500 text-xs mb-1">User ID</p>
|
||||
<p className="font-mono text-sm break-all">{user.id}</p>
|
||||
|
||||
{user.telegram_username && (
|
||||
<>
|
||||
<p className="text-zinc-500 text-xs mt-4 mb-1">Telegram</p>
|
||||
<p className="text-sm">@{user.telegram_username}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-6 py-2 border border-zinc-700 rounded-lg
|
||||
hover:border-zinc-500 transition-colors text-sm"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error display */}
|
||||
{error && (
|
||||
<div className="mt-6 p-4 bg-red-900/20 border border-red-500/50 rounded-lg">
|
||||
<p className="text-red-400 text-sm">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
// API client for Smirk backend
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000';
|
||||
|
||||
export interface ChallengeResponse {
|
||||
challenge: string;
|
||||
challenge_id: string;
|
||||
expires_at: string;
|
||||
}
|
||||
|
||||
export interface AuthResponse {
|
||||
access_token: string;
|
||||
refresh_token: string;
|
||||
expires_in: number;
|
||||
user: {
|
||||
id: string;
|
||||
telegram_id: number | null;
|
||||
telegram_username: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export interface MeResponse {
|
||||
id: string;
|
||||
username: string | null;
|
||||
telegram_id: number | null;
|
||||
telegram_username: string | null;
|
||||
}
|
||||
|
||||
export async function getChallenge(origin: string): Promise<ChallengeResponse> {
|
||||
const res = await fetch(`${API_URL}/api/v1/auth/website/challenge`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ origin }),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ message: 'Request failed' }));
|
||||
throw new Error(error.message || 'Failed to get challenge');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function verifySignature(
|
||||
challengeId: string,
|
||||
signature: { asset: string; signature: string; public_key: string }
|
||||
): Promise<AuthResponse> {
|
||||
const res = await fetch(`${API_URL}/api/v1/auth/website/verify`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
challenge_id: challengeId,
|
||||
signature,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ message: 'Request failed' }));
|
||||
throw new Error(error.message || 'Failed to verify signature');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
|
||||
export async function getMe(token: string): Promise<MeResponse> {
|
||||
const res = await fetch(`${API_URL}/api/v1/auth/me`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Failed to get user info');
|
||||
}
|
||||
|
||||
return res.json();
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// TypeScript types for the Smirk extension's injected window.smirk API
|
||||
|
||||
interface SmirkPublicKeys {
|
||||
btc: string;
|
||||
ltc: string;
|
||||
xmr: string;
|
||||
wow: string;
|
||||
grin: string;
|
||||
}
|
||||
|
||||
interface SmirkSignature {
|
||||
asset: string;
|
||||
signature: string;
|
||||
public_key: string;
|
||||
}
|
||||
|
||||
interface SmirkSignatures {
|
||||
btc: SmirkSignature;
|
||||
ltc: SmirkSignature;
|
||||
xmr: SmirkSignature;
|
||||
wow: SmirkSignature;
|
||||
grin: SmirkSignature;
|
||||
}
|
||||
|
||||
interface SmirkAPI {
|
||||
connect(): Promise<SmirkPublicKeys>;
|
||||
disconnect(): Promise<void>;
|
||||
isConnected(): Promise<boolean>;
|
||||
getPublicKeys(): Promise<SmirkPublicKeys | null>;
|
||||
signMessage(message: string): Promise<SmirkSignatures>;
|
||||
}
|
||||
|
||||
interface Window {
|
||||
smirk?: SmirkAPI;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||