diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn new file mode 100644 index 000000000..1f0f82ccd --- /dev/null +++ b/.clj-kondo/config.edn @@ -0,0 +1,74 @@ +{:output {:exclude-files [".*resources/public/js/compiled.*" + ".*docker/scripts/.*"]} + :linters {:shadowed-fn-param {:level :off} + :shadowed-var {:level :off} + :unused-namespace {:level :off} + :unused-binding {:level :off} + :missing-else-branch {:level :warning} + :clojure-lsp/unused-public-var + {:exclude [user ; REPL utilities + orcpub.styles.core/app + ;; LSP can't trace cross-file references to these — + ;; all are called from classes.cljc (live code) + orcpub.dnd.e5.options/monk-elemental-disciplines + orcpub.dnd.e5.options/spell-tags + orcpub.dnd.e5.options/potent-spellcasting + ;; Live callers exist but LSP can't trace them + orcpub.common/dissoc-in ; events.cljs + orcpub.dnd.e5.character/add-ability-namespaces ; test + ;; Cross-file refs: used in template.cljc but defined in spell_subs.cljs + orcpub.dnd.e5.spell-subs/sunlight-sensitivity + orcpub.dnd.e5.spell-subs/mask-of-the-wild-mod] + ;; re-frame event handlers are dispatched via keyword, not var reference. + ;; LSP can't connect reg-event-db registration to (dispatch [:keyword]). + :exclude-when-defined-by #{re-frame.core/reg-event-db + re-frame.core/reg-event-fx + re-frame.core/reg-sub + re-frame.core/reg-sub-raw}} + ;; garden.selectors vars are generated by macros (defselector, + ;; defpseudoclass, gen-pseudo-class-defs, etc.) at compile time. + ;; clj-kondo can't resolve macro-generated vars statically and + ;; garden doesn't ship a clj-kondo config. + :unresolved-var + {:exclude [garden.selectors + ;; errors.cljc macros behind #?(:clj) reader conditional — + ;; one kondo instance can't resolve them + orcpub.errors]} + ;; read-string is a valid cljs.core symbol that clj-kondo + ;; doesn't recognize in its ClojureScript analysis data. + :unresolved-symbol + {:exclude + [read-string + (clojure.test.check.clojure-test/defspec) + (clojure.core.match/match) + (cljs.core.match/match) + (io.pedestal.interceptor.error/error-dispatch) + (orcpub.modifiers/modifier) + (orcpub.modifiers/deferred-modifier) + (orcpub.modifiers/cum-sum-mod) + (orcpub.modifiers/vec-mod) + (orcpub.modifiers/set-mod) + (orcpub.modifiers/map-mod) + (orcpub.modifiers/fn-mod) + (orcpub.dnd.e5.modifiers/spells-known-cfg) + (orcpub.dnd.e5.modifiers/prop-trait) + (orcpub.dnd.e5.modifiers/dependent-trait) + (orcpub.dnd.e5.modifiers/dependent-trait-2) + (orcpub.dnd.e5.modifiers/skill-proficiency) + (orcpub.dnd.e5.modifiers/skill-expertise) + (orcpub.dnd.e5.modifiers/tool-expertise) + (orcpub.dnd.e5.modifiers/ac-bonus-fn) + (orcpub.dnd.e5.modifiers/attack) + (orcpub.dnd.e5.modifiers/action) + (orcpub.dnd.e5.modifiers/bonus-action) + (orcpub.dnd.e5.modifiers/reaction) + (orcpub.dnd.e5.modifiers/level-val) + (orcpub.entity-spec/make-entity) + (orcpub.routes-test/with-conn) + (orcpub.routes.folder-test/with-conn) + (user/with-db)]}} + :lint-as {reagent.core/with-let clojure.core/let + hiccup.def/defhtml clojure.core/defn + user/with-db clojure.core/let + clojure.test.check.clojure-test/defspec clojure.test/deftest} +} diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..2be7f65ff --- /dev/null +++ b/.dockerignore @@ -0,0 +1,11 @@ +# ignore .git and .cache folders +.git +.cache +.idea +.github +docker-compose* +test/* +*.md +data/* +log/* +backups/* \ No newline at end of file diff --git a/.ebextensions/https-instance-securitygroup.config b/.ebextensions/https-instance-securitygroup.config deleted file mode 100644 index 578ee1f92..000000000 --- a/.ebextensions/https-instance-securitygroup.config +++ /dev/null @@ -1,9 +0,0 @@ -Resources: - sslSecurityGroupIngress: - Type: AWS::EC2::SecurityGroupIngress - Properties: - GroupId: {"Fn::GetAtt" : ["AWSEBSecurityGroup", "GroupId"]} - IpProtocol: tcp - ToPort: 443 - FromPort: 443 - CidrIp: 0.0.0.0/0 \ No newline at end of file diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..4444c8eba --- /dev/null +++ b/.env.example @@ -0,0 +1,42 @@ +# ============================================================================ +# Dungeon Master's Vault — Docker Environment Configuration +# +# Copy this file to .env and update the values: +# cp .env.example .env +# +# Or run the setup script to generate .env with secure random values: +# ./docker-setup.sh +# ============================================================================ + +# --- Application --- +PORT=8890 + +# --- Datomic Database --- +# ADMIN_PASSWORD secures the Datomic admin interface +# DATOMIC_PASSWORD is used by the application to connect to Datomic +# The password in DATOMIC_URL must match DATOMIC_PASSWORD +ADMIN_PASSWORD=change-me-admin +DATOMIC_PASSWORD=change-me-datomic +DATOMIC_URL=datomic:free://datomic:4334/orcpub?password=change-me-datomic + +# --- Security --- +# Secret used to sign JWT tokens (20+ characters recommended) +SIGNATURE=change-me-to-something-unique-and-long + +# --- Email (SMTP) --- +# Leave EMAIL_SERVER_URL empty to disable email functionality +EMAIL_SERVER_URL= +EMAIL_ACCESS_KEY= +EMAIL_SECRET_KEY= +EMAIL_SERVER_PORT=587 +EMAIL_FROM_ADDRESS= +EMAIL_ERRORS_TO= +EMAIL_SSL=FALSE +EMAIL_TLS=FALSE + +# --- Initial Admin User (optional) --- +# Set these then run: ./docker-user.sh init +# Safe to run multiple times — duplicates are skipped. +INIT_ADMIN_USER= +INIT_ADMIN_EMAIL= +INIT_ADMIN_PASSWORD= diff --git a/.expo/packager-info.json b/.expo/packager-info.json deleted file mode 100644 index 1938f58c8..000000000 --- a/.expo/packager-info.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "expoServerPort": 19000, - "packagerPort": 19001, - "packagerPid": 28823, - "expoServerNgrokUrl": "https://a2-dgb.larrychristensen.orcpub.exp.direct", - "packagerNgrokUrl": "https://packager.a2-dgb.larrychristensen.orcpub.exp.direct", - "ngrokPid": 28936 -} \ No newline at end of file diff --git a/.expo/settings.json b/.expo/settings.json deleted file mode 100644 index 310d971b0..000000000 --- a/.expo/settings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "hostType": "tunnel", - "lanType": "ip", - "dev": true, - "strict": false, - "minify": false, - "urlType": "exp", - "urlRandomness": "a2-dgb" -} \ No newline at end of file diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 000000000..0b5ed6667 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,2 @@ +# These are supported funding model platforms +patreon: DungeonMastersVault diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..2fbb87894 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,43 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** + +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** + +A clear and concise description of what you expected to happen. + +**Screenshots** + +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information):** + + - OS: [e.g. iOS] + - Browser [e.g. chrome, safari] + - Version [e.g. 22] + +**Smartphone (please complete the following information):** + + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - Browser [e.g. stock browser, safari] + - Version [e.g. 22] + +**Additional context** + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..0eff7ce6e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +**Motivation / Use Case** + +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Expected Behavior** + +A clear and concise description of what you want to happen. + +**Other Information** + +A clear and concise description of any alternative solutions or features you've considered. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 000000000..5baec0250 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,10 @@ +## Description: + +**Related issue (if applicable):** fixes # + +## Checklist: + - [ ] The code change is tested and works locally. + - [ ] I have commented my code, particularly in hard-to-understand areas + - [ ] I have made corresponding changes to the documentation if necessary + - [ ] There is no commented out code in this PR. + - [ ] My changes generate no new warnings (check the console) \ No newline at end of file diff --git a/.github/screenshot.png b/.github/screenshot.png new file mode 100644 index 000000000..a2ab4b08e Binary files /dev/null and b/.github/screenshot.png differ diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml new file mode 100644 index 000000000..56db5e5f7 --- /dev/null +++ b/.github/workflows/continuous-integration.yml @@ -0,0 +1,245 @@ +# Dual-stack CI: auto-detects Java 8 (develop) vs Java 21 (breaking/) based +# on whether dev.cljs.edn exists in the checkout. After breaking/ merges to +# develop, the Java 8 path becomes dead code and can be removed. + +name: CI + +on: + push: + branches: [develop, main] + pull_request: + branches: [develop, main] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + checks: write + +jobs: + detect-stack: + name: Detect Stack + runs-on: ubuntu-latest + outputs: + java-version: ${{ steps.detect.outputs.java-version }} + java-distribution: ${{ steps.detect.outputs.java-distribution }} + lein-version: ${{ steps.detect.outputs.lein-version }} + cljs-command: ${{ steps.detect.outputs.cljs-command }} + needs-datomic-pro: ${{ steps.detect.outputs.needs-datomic-pro }} + stack-label: ${{ steps.detect.outputs.stack-label }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect stack from project files + id: detect + run: | + # dev.cljs.edn is the figwheel-main build config — only exists on + # breaking/2026-stack-modernization (Java 21, Datomic Pro, fig:build). + if [ -f "dev.cljs.edn" ]; then + echo "Detected: figwheel-main stack (Java 21, Datomic Pro)" + echo "java-version=21" >> $GITHUB_OUTPUT + echo "java-distribution=temurin" >> $GITHUB_OUTPUT + echo "lein-version=2.11.2" >> $GITHUB_OUTPUT + echo "cljs-command=fig:build" >> $GITHUB_OUTPUT + echo "needs-datomic-pro=true" >> $GITHUB_OUTPUT + echo "stack-label=Java 21 / figwheel-main / Datomic Pro" >> $GITHUB_OUTPUT + else + echo "Detected: legacy stack (Java 8, Datomic Free)" + echo "java-version=8" >> $GITHUB_OUTPUT + echo "java-distribution=zulu" >> $GITHUB_OUTPUT + echo "lein-version=2.9.10" >> $GITHUB_OUTPUT + echo "cljs-command=cljsbuild once dev" >> $GITHUB_OUTPUT + echo "needs-datomic-pro=false" >> $GITHUB_OUTPUT + echo "stack-label=Java 8 / cljsbuild / Datomic Free" >> $GITHUB_OUTPUT + fi + + test: + name: Test & Lint (${{ needs.detect-stack.outputs.stack-label }}) + runs-on: ubuntu-latest + needs: detect-stack + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up JDK ${{ needs.detect-stack.outputs.java-version }} + uses: actions/setup-java@v4 + with: + distribution: ${{ needs.detect-stack.outputs.java-distribution }} + java-version: ${{ needs.detect-stack.outputs.java-version }} + + - name: Set up Leiningen + uses: DeLaGuardo/setup-clojure@12.5 + with: + lein: ${{ needs.detect-stack.outputs.lein-version }} + + - name: Cache Maven dependencies + uses: actions/cache@v4 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-j${{ needs.detect-stack.outputs.java-version }}-${{ hashFiles('project.clj') }} + restore-keys: | + ${{ runner.os }}-maven-j${{ needs.detect-stack.outputs.java-version }}- + + - name: Load local libs (pdfbox) + run: | + mkdir -p ~/.m2/repository/org/ + cp -rv ./lib/org/* ~/.m2/repository/org/ + + # Datomic Pro jars are not committed to git. Download and maven-install + # them the same way .devcontainer/post-create.sh does. + - name: Install Datomic Pro + if: needs.detect-stack.outputs.needs-datomic-pro == 'true' + run: | + DATOMIC_VERSION=1.0.7482 + TARGET_DIR="lib/com/datomic/datomic-pro/${DATOMIC_VERSION}" + ZIP_PATH="/tmp/datomic-pro-${DATOMIC_VERSION}.zip" + DOWNLOAD_URL="https://datomic-pro-downloads.s3.amazonaws.com/${DATOMIC_VERSION}/datomic-pro-${DATOMIC_VERSION}.zip" + + echo "Downloading Datomic Pro ${DATOMIC_VERSION}..." + curl --fail --location --silent --show-error -o "$ZIP_PATH" "$DOWNLOAD_URL" + + mkdir -p "${TARGET_DIR}" + unzip -q "$ZIP_PATH" -d "${TARGET_DIR}" + + # Flatten nested directory if present (zip contains datomic-pro-VERSION/ subdir) + TOP_SUBDIR=$(find "${TARGET_DIR}" -mindepth 1 -maxdepth 1 -type d -print -quit || true) + if [ -n "${TOP_SUBDIR}" ] && [ -z "$(find "${TARGET_DIR}" -maxdepth 1 -type f -print -quit)" ]; then + mv "${TOP_SUBDIR}"/* "${TARGET_DIR}/" + rmdir "${TOP_SUBDIR}" + fi + + # Populate ~/.m2 with Datomic Pro artifacts + (cd "${TARGET_DIR}" && bash bin/maven-install) + echo "Datomic Pro ${DATOMIC_VERSION} installed to local Maven repo" + + - name: Install dependencies + run: lein deps + + - name: Run linter + id: lint + run: | + echo "## Lint Results" >> $GITHUB_STEP_SUMMARY + if lein lint 2>&1 | tee lint-output.txt; then + echo "**Lint passed** - no errors" >> $GITHUB_STEP_SUMMARY + else + echo "**Lint failed**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + cat lint-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Run tests + id: test + run: | + echo "## Test Results" >> $GITHUB_STEP_SUMMARY + if lein test 2>&1 | tee test-output.txt; then + SUMMARY=$(grep -E "^Ran [0-9]+ tests" test-output.txt || echo "Tests completed") + echo "**Tests passed**" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "$SUMMARY" >> $GITHUB_STEP_SUMMARY + else + echo "**Tests failed**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -50 test-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Build ClojureScript + id: cljs + run: | + echo "## ClojureScript Build" >> $GITHUB_STEP_SUMMARY + if lein ${{ needs.detect-stack.outputs.cljs-command }} 2>&1 | tee cljs-output.txt; then + echo "**CLJS build succeeded**" >> $GITHUB_STEP_SUMMARY + else + echo "**CLJS build failed**" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + tail -50 cljs-output.txt >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + exit 1 + fi + + - name: Post PR comment with results + if: github.event_name == 'pull_request' && always() + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + + const lintOutput = fs.existsSync('lint-output.txt') ? fs.readFileSync('lint-output.txt', 'utf8') : 'No output'; + const testOutput = fs.existsSync('test-output.txt') ? fs.readFileSync('test-output.txt', 'utf8') : 'No output'; + const cljsOutput = fs.existsSync('cljs-output.txt') ? fs.readFileSync('cljs-output.txt', 'utf8') : 'No output'; + + const lintOk = '${{ steps.lint.outcome }}' === 'success'; + const testOk = '${{ steps.test.outcome }}' === 'success'; + const cljsOk = '${{ steps.cljs.outcome }}' === 'success'; + const allOk = lintOk && testOk && cljsOk; + const stackLabel = '${{ needs.detect-stack.outputs.stack-label }}'; + + const testMatch = testOutput.match(/Ran (\d+) tests containing (\d+) assertions/); + const testSummary = testMatch ? `${testMatch[1]} tests, ${testMatch[2]} assertions` : 'See logs'; + + const status = allOk ? 'All checks passed' : 'Some checks failed'; + const body = [ + `## ${status}`, + '', + '| Check | Status | Details |', + '|-------|--------|---------|', + `| Lint | ${lintOk ? 'Pass' : 'Fail'} | ${lintOk ? 'No errors' : 'See workflow logs'} |`, + `| Tests | ${testOk ? 'Pass' : 'Fail'} | ${testOk ? testSummary : 'See workflow logs'} |`, + `| CLJS Build | ${cljsOk ? 'Pass' : 'Fail'} | ${cljsOk ? 'Compiled' : 'See workflow logs'} |`, + '', + `**Stack**: ${stackLabel}`, + '', + '
', + 'Full test output', + '', + '```', + testOutput.slice(-2000), + '```', + '', + '
', + '', + '---', + `*[Workflow run](${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId})*` + ].join('\n'); + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number + }); + + const botComment = comments.find(c => + c.user.type === 'Bot' && (c.body.includes('All checks passed') || c.body.includes('Some checks failed')) + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: body + }); + } + + - name: Upload artifacts on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: ci-failure-logs + path: | + lint-output.txt + test-output.txt + cljs-output.txt + retention-days: 7 diff --git a/.github/workflows/docker-integration.yml b/.github/workflows/docker-integration.yml new file mode 100644 index 000000000..0c9c86c65 --- /dev/null +++ b/.github/workflows/docker-integration.yml @@ -0,0 +1,342 @@ +# Docker integration tests: script validation always runs, container tests +# only run when Docker Hub images are available (orcpub/orcpub:latest may +# not exist for all release cycles). + +name: Docker Integration Test + +on: + pull_request: + branches: [develop] + paths: + - 'docker/**' + - 'docker-compose*.yaml' + - 'docker-setup.sh' + - 'docker-user.sh' + - 'deploy/**' + - '.github/workflows/docker-integration.yml' + workflow_dispatch: + +jobs: + docker-test: + name: Docker Setup & User Management + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + # ── Script validation (always runs) ────────────────────────── + + - name: Lint shell scripts + run: | + sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck + shellcheck docker-setup.sh docker-user.sh + + - name: Run docker-setup.sh --auto + run: | + ./docker-setup.sh --auto + echo "--- Generated .env (secrets redacted) ---" + sed 's/=.*/=***/' .env + + - name: Validate .env password consistency + run: | + PW=$(grep '^DATOMIC_PASSWORD=' .env | cut -d= -f2) + URL_PW=$(grep '^DATOMIC_URL=' .env | sed 's/.*password=//') + if [ "$PW" != "$URL_PW" ]; then + echo "FAIL: DATOMIC_PASSWORD and DATOMIC_URL password mismatch" + exit 1 + fi + echo "OK: Passwords match" + + - name: Test — setup --force preserves existing values + run: | + # Save original passwords + ORIG_ADMIN=$(grep '^ADMIN_PASSWORD=' .env | cut -d= -f2) + ORIG_DATOMIC=$(grep '^DATOMIC_PASSWORD=' .env | cut -d= -f2) + ORIG_SIG=$(grep '^SIGNATURE=' .env | cut -d= -f2) + + # Re-run with --force --auto (should regenerate) + ./docker-setup.sh --auto --force + + # Verify .env was regenerated (new passwords, since --auto generates fresh ones) + NEW_ADMIN=$(grep '^ADMIN_PASSWORD=' .env | cut -d= -f2) + NEW_DATOMIC=$(grep '^DATOMIC_PASSWORD=' .env | cut -d= -f2) + + # Verify structure is intact + grep -q '^DATOMIC_URL=' .env || { echo "FAIL: DATOMIC_URL missing"; exit 1; } + grep -q '^SIGNATURE=' .env || { echo "FAIL: SIGNATURE missing"; exit 1; } + grep -q '^PORT=' .env || { echo "FAIL: PORT missing"; exit 1; } + + # Re-check password consistency after --force + PW=$(grep '^DATOMIC_PASSWORD=' .env | cut -d= -f2) + URL_PW=$(grep '^DATOMIC_URL=' .env | sed 's/.*password=//') + if [ "$PW" != "$URL_PW" ]; then + echo "FAIL: Password mismatch after --force re-run" + exit 1 + fi + echo "OK: --force regenerated .env with consistent passwords" + + # ── Container tests (only if images are available) ─────────── + + - name: Pull container images + id: pull + continue-on-error: true + run: docker compose pull + + - name: Start datomic (no deps) + if: steps.pull.outcome == 'success' + run: | + docker compose up -d --no-deps datomic + echo "Datomic container started, waiting for health..." + + - name: Wait for datomic healthy + if: steps.pull.outcome == 'success' + run: | + for i in $(seq 1 90); do + CID=$(docker compose ps -q datomic 2>/dev/null) || true + if [ -z "$CID" ]; then + echo " [$i/90] datomic container not found yet" + sleep 2 + continue + fi + RUNNING=$(docker inspect --format='{{.State.Running}}' "$CID" 2>/dev/null || echo "false") + STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$CID" 2>/dev/null || echo "starting") + echo " [$i/90] running=$RUNNING health=$STATUS" + if [ "$STATUS" = "healthy" ]; then + echo "Datomic is healthy (after ~$((i * 2))s)" + break + fi + if [ "$RUNNING" = "false" ]; then + echo "WARN: datomic container stopped — dumping logs" + docker compose logs datomic + echo "Container will restart (restart: always), continuing to wait..." + fi + if [ "$i" -eq 90 ]; then + echo "FAIL: Datomic did not become healthy within 180s" + echo "=== container state ===" + docker inspect --format='{{json .State}}' "$CID" | python3 -m json.tool || true + echo "=== datomic logs ===" + docker compose logs datomic + exit 1 + fi + sleep 2 + done + + - name: Start orcpub and web + if: steps.pull.outcome == 'success' + run: | + docker compose up -d + docker compose ps + + - name: Wait for orcpub healthy + if: steps.pull.outcome == 'success' + run: | + for i in $(seq 1 90); do + CID=$(docker compose ps -q orcpub 2>/dev/null) || true + if [ -z "$CID" ]; then + echo " [$i/90] orcpub container not found yet" + sleep 2 + continue + fi + STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$CID" 2>/dev/null || echo "starting") + echo " [$i/90] orcpub health=$STATUS" + if [ "$STATUS" = "healthy" ]; then + echo "orcpub is healthy (after ~$((i * 2))s)" + break + fi + if [ "$STATUS" = "unhealthy" ]; then + echo "FAIL: orcpub reported unhealthy" + echo "=== all logs ===" + docker compose logs + exit 1 + fi + if [ "$i" -eq 90 ]; then + echo "FAIL: orcpub did not become healthy within 180s" + docker compose logs + exit 1 + fi + sleep 2 + done + docker compose ps + + - name: Test — create user + if: steps.pull.outcome == 'success' + run: | + ./docker-user.sh create testadmin admin@test.local SecurePass123 + echo "Exit code: $?" + + - name: Test — check user exists + if: steps.pull.outcome == 'success' + run: | + OUTPUT=$(./docker-user.sh check testadmin) + echo "$OUTPUT" + echo "$OUTPUT" | grep -q "testadmin" + echo "$OUTPUT" | grep -q "admin@test.local" + echo "$OUTPUT" | grep -q "true" # verified + + - name: Test — list includes user + if: steps.pull.outcome == 'success' + run: | + OUTPUT=$(./docker-user.sh list) + echo "$OUTPUT" + echo "$OUTPUT" | grep -q "testadmin" + + - name: Test — duplicate user fails + if: steps.pull.outcome == 'success' + run: | + if ./docker-user.sh create testadmin admin@test.local SecurePass123 2>&1; then + echo "FAIL: Should have rejected duplicate user" + exit 1 + fi + echo "OK: Duplicate user correctly rejected" + + - name: Test — create second user + if: steps.pull.outcome == 'success' + run: ./docker-user.sh create player2 player2@test.local AnotherPass456 + + - name: Test — list shows both users + if: steps.pull.outcome == 'success' + run: | + OUTPUT=$(./docker-user.sh list) + echo "$OUTPUT" + echo "$OUTPUT" | grep -q "testadmin" + echo "$OUTPUT" | grep -q "player2" + + - name: Test — verify already-verified user is idempotent + if: steps.pull.outcome == 'success' + run: | + OUTPUT=$(./docker-user.sh verify testadmin) + echo "$OUTPUT" + echo "$OUTPUT" | grep -q "already verified" + + - name: Test — batch create users (with duplicates) + if: steps.pull.outcome == 'success' + run: | + cat > /tmp/test-users.txt <<'TXT' + # Test batch file + batch1 batch1@test.local BatchPass111 + batch2 batch2@test.local BatchPass222 + # This next line is a duplicate from earlier single-create test + testadmin admin@test.local SecurePass123 + TXT + OUTPUT=$(./docker-user.sh batch /tmp/test-users.txt) + echo "$OUTPUT" + echo "$OUTPUT" | grep -q "batch1" + echo "$OUTPUT" | grep -q "batch2" + echo "$OUTPUT" | grep -q "SKIP.*testadmin" + echo "$OUTPUT" | grep -q "2 created" + echo "$OUTPUT" | grep -q "1 skipped (duplicate)" + echo "$OUTPUT" | grep -q "0 failed" + echo "OK: Batch created 2 new, skipped 1 duplicate" + + - name: Test — batch users appear in list + if: steps.pull.outcome == 'success' + run: | + OUTPUT=$(./docker-user.sh list) + echo "$OUTPUT" + echo "$OUTPUT" | grep -q "batch1" + echo "$OUTPUT" | grep -q "batch2" + + - name: Test — init creates admin from .env + if: steps.pull.outcome == 'success' + run: | + # Append INIT_ADMIN_* vars to .env + printf '\nINIT_ADMIN_USER=initadmin\nINIT_ADMIN_EMAIL=initadmin@test.local\nINIT_ADMIN_PASSWORD=InitPass789\n' >> .env + + # Run init + OUTPUT=$(./docker-user.sh init) + echo "$OUTPUT" + echo "$OUTPUT" | grep -q "initadmin" + + # Verify user was created + CHECK=$(./docker-user.sh check initadmin) + echo "$CHECK" + echo "$CHECK" | grep -q "initadmin" + echo "$CHECK" | grep -q "initadmin@test.local" + echo "$CHECK" | grep -q "true" + echo "OK: init created admin from .env" + + - name: Test — init is idempotent (re-run skips existing) + if: steps.pull.outcome == 'success' + run: | + # Running init again should not fail — duplicate is handled + if ./docker-user.sh init 2>&1; then + echo "FAIL: init should exit non-zero for duplicate user" + exit 1 + fi + echo "OK: init correctly reports duplicate on re-run" + + - name: Test — check nonexistent user fails + if: steps.pull.outcome == 'success' + run: | + if ./docker-user.sh check nobody@nowhere.com 2>&1; then + echo "FAIL: Should have reported user not found" + exit 1 + fi + echo "OK: Nonexistent user correctly not found" + + - name: Test — created user can log in via HTTP + if: steps.pull.outcome == 'success' + run: | + # Use nginx (port 443) since orcpub:8890 is not exposed to host + RESPONSE=$(curl -sk -X POST https://localhost/login \ + -H "Content-Type: application/json" \ + -d '{"username":"testadmin","password":"SecurePass123"}' \ + -w "\n%{http_code}" 2>&1) || true + + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + + echo "HTTP $HTTP_CODE" + echo "$BODY" + + if [ "$HTTP_CODE" = "200" ]; then + echo "OK: Login succeeded" + echo "$BODY" | grep -q "token" + echo "OK: Response contains JWT token" + else + echo "FAIL: Expected HTTP 200, got $HTTP_CODE" + exit 1 + fi + + - name: Test — wrong password is rejected + if: steps.pull.outcome == 'success' + run: | + HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" \ + -X POST https://localhost/login \ + -H "Content-Type: application/json" \ + -d '{"username":"testadmin","password":"WrongPassword"}' 2>&1) || true + + echo "HTTP $HTTP_CODE" + if [ "$HTTP_CODE" = "401" ]; then + echo "OK: Wrong password correctly rejected" + else + echo "FAIL: Expected HTTP 401, got $HTTP_CODE" + exit 1 + fi + + # ── Always-run steps ───────────────────────────────────────── + + - name: Container tests skipped (images unavailable) + if: steps.pull.outcome == 'failure' + run: | + echo "## Docker Integration" >> $GITHUB_STEP_SUMMARY + echo "Container images not available on Docker Hub." >> $GITHUB_STEP_SUMMARY + echo "Script validation passed. Container tests skipped." >> $GITHUB_STEP_SUMMARY + + - name: Collect logs on failure + if: failure() + run: | + echo "=== docker compose ps ===" + docker compose ps + echo "=== datomic logs ===" + docker compose logs datomic + echo "=== orcpub logs ===" + docker compose logs orcpub + echo "=== web logs ===" + docker compose logs web + + - name: Cleanup + if: always() + run: docker compose down -v diff --git a/.github/workflows/docker.yaml b/.github/workflows/docker.yaml new file mode 100644 index 000000000..7800a702d --- /dev/null +++ b/.github/workflows/docker.yaml @@ -0,0 +1,123 @@ +name: Docker Releases + +# 1️⃣ Trigger on tag pushes and manual dispatches +on: + push: + tags: + - '*' # e.g. 1.0, 2.3.4.4 + workflow_dispatch: + inputs: + tag: + description: "Tag name to build (e.g., 1.2.3.4)" + required: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + + # -------------------------------------------------------------------- + # Get the version string – works for both push and dispatch + # -------------------------------------------------------------------- + - name: Get Version + id: get_version + run: | + if [[ -n "${{ github.event.inputs.tag }}" ]]; then + VERSION="${{ github.event.inputs.tag }}" + else + VERSION="${GITHUB_REF#refs/tags/}" + fi + echo "VERSION=${VERSION}" >> $GITHUB_OUTPUT + + # -------------------------------------------------------------------- + # Checkout the repo (HTTPS – no SSH key needed) + # -------------------------------------------------------------------- + - name: Checkout + uses: actions/checkout@v3 + with: + ref: ${{ github.event.inputs.tag && format('refs/tags/{0}', github.event.inputs.tag) || github.ref }} + + # -------------------------------------------------------------------- + # Update version/date in src/cljs/orcpub/ver.cljc + # -------------------------------------------------------------------- + - name: Update src/cljs/orcpub/ver.cljc + run: | + sed -i 's/defn version \[\] ".*"/defn version [] "${{ steps.get_version.outputs.VERSION }}"/' src/cljs/orcpub/ver.cljc + sed -i 's/defn date \[\] ".*"/defn date [] "$(date +%m-%d-%Y)"/' src/cljs/orcpub/ver.cljc + cat src/cljs/orcpub/ver.cljc + + # -------------------------------------------------------------------- + # Docker: login, QEMU, Buildx + # -------------------------------------------------------------------- + - name: Login to DockerHub + uses: docker/login-action@v2 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + # -------------------------------------------------------------------- + # Build & push the “latest” image for orcpub + # -------------------------------------------------------------------- + - name: Build and push latest orcpub + uses: docker/build-push-action@v3 + with: + context: . + file: ./docker/orcpub/Dockerfile + platforms: linux/amd64 + push: true + tags: orcpub/orcpub:latest + + # -------------------------------------------------------------------- + # Build & push the versioned image for orcpub + # -------------------------------------------------------------------- + - name: Build and push ${{ steps.get_version.outputs.VERSION }} orcpub + uses: docker/build-push-action@v3 + with: + context: . + file: ./docker/orcpub/Dockerfile + platforms: linux/amd64 + push: true + tags: orcpub/orcpub:release-${{ steps.get_version.outputs.VERSION }} + + # -------------------------------------------------------------------- + # Build & push the “latest” image for datomic + # -------------------------------------------------------------------- + - name: Build and push latest datomic + uses: docker/build-push-action@v3 + with: + context: . + file: ./docker/datomic/Dockerfile + platforms: linux/amd64 + push: true + tags: orcpub/datomic:latest + + # -------------------------------------------------------------------- + # Build & push the versioned image for datomic + # -------------------------------------------------------------------- + - name: Build and push ${{ steps.get_version.outputs.VERSION }} datomic + uses: docker/build-push-action@v3 + with: + context: . + file: ./docker/datomic/Dockerfile + platforms: linux/amd64 + push: true + tags: orcpub/datomic:release-${{ steps.get_version.outputs.VERSION }} + + # -------------------------------------------------------------------- + # Commit the updated version file back to *develop* + # -------------------------------------------------------------------- + - name: Commit updated ver.cljc + run: | + git config user.name github-actions + git config user.email github-actions@github.com + git fetch --all + git checkout develop || git checkout -b develop + git add src/cljs/orcpub/ver.cljc + git commit -m "Update version to ${{ steps.get_version.outputs.VERSION }}" + git push \ No newline at end of file diff --git a/.gitignore b/.gitignore index 402fb7e4c..80f856836 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,8 @@ -/resources/public/js/compiled/** +.editorconfig +.gitattributes +/resources/public/css/compiled +/resources/public/js/compiled +/resources/*_backup.pdf figwheel_server.log pom.xml *jar @@ -7,17 +11,44 @@ pom.xml /out/ /target/ /node_modules/ +.clj-kondo +.calva .lein-deps-sum .lein-repl-history .lein-plugins/ .lein-* +.lsp .idea orcpub.iml profiles.clj env.sh +.env .repl .nrepl-port -/resources/*_backup.pdf *~ # *#* +/data +*.crt +*.key +deploy/homebrew/* + +# Local working files +.integration-workflow-state +branch-sync-plan.md +dev/test-accounts.edn + +# E2E test artifacts +/e2e/node_modules/ +/e2e/playwright-report/ +/e2e/test-results/ + +# Clojure CLI cache +.cpcache +cljs-test-runner-out + +# As created by some LSP-protocol tooling, e.g. nvim-lsp +.lsp + +# Claude Code local session data +.claude-data/ diff --git a/.grenrc.js b/.grenrc.js new file mode 100644 index 000000000..36c0f5629 --- /dev/null +++ b/.grenrc.js @@ -0,0 +1,16 @@ +module.exports = { + "prefix": "v", + "ignoreIssuesWith": [ + "duplicate", + "wontfix", + "invalid", + "help wanted" + ], + "template": { + "issue": "- [{{text}}]({{url}}) {{name}}" + }, + "groupBy": { + "Enhancements:": ["enhancement", "internal"], + "Bug Fixes:": ["bug"] + } +}; \ No newline at end of file diff --git a/.lsp/config.edn b/.lsp/config.edn new file mode 100644 index 000000000..b101d323b --- /dev/null +++ b/.lsp/config.edn @@ -0,0 +1,6 @@ +{;; Explicit source-paths override classpath discovery. + ;; Without this, clojure-lsp scans `resources/` (a :resource-path) + ;; and lints compiled CLJS output in resources/public/js/compiled/out/. + :source-paths #{"src/clj" "src/cljc" "src/cljs" "web/cljs" + "test/clj" "test/cljc" "test/cljs" "dev"} + :source-paths-ignore-regex ["resources/public/js/compiled"]} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 079130527..000000000 --- a/.travis.yml +++ /dev/null @@ -1 +0,0 @@ -language: clojure diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..98c9ccec0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,191 @@ +# Changelog: Error Handling, Import Validation & Content Reconciliation + +All changes target `develop` from `feature/error-handling-import-validation`. + +--- + +## New Features + +### Import Validation (`import_validation.cljs` -- new file) +- **Unicode normalization**: Converts smart quotes, em-dashes, non-breaking spaces, and 40+ other problematic Unicode characters to ASCII equivalents on import and homebrew save. Prevents copy-paste corruption from Word/Google Docs. +- **Required field detection & auto-fill**: On import, missing required fields (`:name`, `:hit-die`, `:speed`, etc.) are auto-filled with placeholder values like `[Missing Name]`. Content types covered: classes, subclasses, races, subraces, backgrounds, feats, spells, monsters, invocations, languages, encounters. +- **Trait validation**: Nested `:traits` arrays are checked for missing `:name` fields and auto-filled. +- **Option validation**: Empty options (`{}`) created by the UI are detected and auto-filled with unique default names ("Option 1", "Option 2", etc.). +- **Multi-plugin format detection**: Distinguishes single-plugin from multi-source orcbrew files for correct processing. + +### Export Validation +- **Pre-export warning modal**: Before exporting homebrew, all content is validated for missing required fields. If issues are found, a modal lists them with an "Export Anyway" option. +- **Specific save error messages**: `reg-save-homebrew` now extracts field names from spec failures and shows targeted messages instead of generic "You must specify a name" errors. + +### Content Reconciliation (`content_reconciliation.cljs` -- new file) +- **Missing content detection**: When a character references homebrew content that isn't loaded (e.g., deleted plugin), the system detects missing races, classes, and subclasses. +- **Fuzzy key matching**: Uses prefix matching and base-keyword similarity to suggest available content that resembles missing keys (top 5 matches with similarity scores). +- **Source inference**: Guesses which plugin pack a missing key likely came from based on key structure. + +### Missing Content Warning UI (`character_builder.cljs`) +- **Warning banner**: Orange expandable banner appears in character builder when content is missing, showing count and details. +- **Detail panel**: Lists each missing item with its content type, key, inferred source, and suggestions for similar available content. +- **DOM IDs for testability**: `#missing-content-warning`, `#missing-content-details`, `.missing-content-item` with `data-key` and `data-type` attributes. + +### Conflict Resolution Modal (`views/conflict_resolution.cljs`, `events.cljs`) +- **Duplicate key detection**: On import, detects keys that conflict with already-loaded homebrew (both internal duplicates within a file and external conflicts with existing content). +- **Resolution UI**: Modal presents each conflict with rename options. Key renaming updates internal references (subclass -> parent class mappings, etc.). +- **Color-coded radio options**: Rename (cyan), Keep (orange), Skip (purple) with left-border + tinted background. All styles in Garden CSS. + +### Import Log Panel (`views/import_log.cljs`) +- **Grouped collapsible sections**: Changes grouped into Key Renames, Field Fixes, Data Cleanup, and Advanced Details (collapsed by default). Empty sections hidden automatically. +- **Detailed field fix reporting**: Field Fixes section shows per-item breakdown — which item, content type, which fields were filled, how many traits/options were fixed. +- **Collapsible section component**: Reusable `collapsible-section` with configurable icon, colors, and default-expanded state. + +### OrcBrew CLI Debug Tool (`tools/orcbrew.clj` -- new file) +- `lein prettify-orcbrew ` -- Pretty-prints orcbrew EDN for readability. +- `lein prettify-orcbrew --analyze` -- Reports potential issues: nil-nil patterns, problematic Unicode, disabled entries, missing trait names, file structure summary. + +--- + +## Bug Fixes + +### nil nil Corruption (`events.cljs`) +- **Root cause fix**: `set-class-path-prop` was calling `assoc-in` with a nil path, producing `{nil nil}` entries in character data. Now guards against nil path before the second `assoc-in`. + +### Nil Character ID Crash (`views.cljs`) +- Character list page crashed with "Cannot form URI without a value given for :id parameter" when characters had nil `:db/id`. Added `(when id ...)` guard to skip rendering those entries. + +### Subclass Key Preservation (`options.cljc`, `spell_subs.cljs`) +- Subclass processing now uses explicit `:key` field if present (for renamed plugins), falling back to name-generated key. Prevents renamed keys from reverting. +- `plugin-subclasses` subscription preserves map keys and sets `:key` on subclass data correctly. + +### Plugin Data Robustness (`spell_subs.cljs`) +- `plugin-vals` subscription wrapped in try-catch to skip malformed plugin data instead of crashing. +- `level-modifier` handles unknown modifier types gracefully (logs warning, returns nil instead of throwing). +- `make-levels` filters out nil modifiers with `keep`. + +### Unhandled HTTP Status Crash (`subs.cljs`, `equipment_subs.cljs`) +- All 7 API-calling subscriptions used bare `case` on HTTP status with no default clause. Any unexpected status (e.g., 400) threw `No matching clause`. Replaced with `handle-api-response` HOF that logs unhandled statuses to console. + +### Import Log "Renamed key nil -> nil" (`events.cljs`, `import_validation.cljs`) +- Key rename change entries used `:old-key`/`:new-key` fields but display code expected `:from`/`:to`. Unified on `:from`/`:to` across creation, application, and display. + +--- + +## Error Handling (Backend) + +### Database (`datomic.clj`) +- Startup wrapped in try-catch with structured errors: `:missing-db-uri`, `:db-connection-failed`, `:schema-initialization-failed`. + +### Email (`email.clj`) +- Email config parsing catches `NumberFormatException` for invalid port (`:invalid-port`). +- `send-verification-email` and `send-reset-email` check postal response and raise on failure (`:verification-email-failed`). + +### PDF Generation (`pdf.clj`, `pdf_spec.cljc`) +- Network timeouts (10s connect, 10s read) for image loading. Specific handling for `SocketTimeoutException` and `UnknownHostException`. +- Nil guards throughout `pdf_spec.cljc`: `total-length`, `trait-string`, `resistance-strings`, `profs-paragraph`, `keyword-vec-trait`, `damage-str`, spell name lookup. All use fallback strings like "(unknown)", "(Unknown Spell)", "(Unnamed Trait)". + +### Routes (`routes.clj`, `routes/party.clj`) +- All mutation endpoints wrapped with error handling: verification, password reset, entity CRUD, party operations. Each uses structured error codes (`:verification-failed`, `:entity-creation-failed`, `:party-creation-failed`, etc.). + +### System (`system.clj`) +- PORT environment variable parsing validates numeric input (`:invalid-port`). + +### Error Infrastructure (`errors.cljc` -- expanded) +- New error code constants for auth flows. +- `log-error`, `create-error` utility functions. +- `with-db-error-handling`, `with-email-error-handling`, `with-validation` macros for consistent patterns. + +--- + +## Supporting Changes + +### Common Utilities (`common.cljc`) +- `kw-base`: Extracts keyword base before first dash (e.g., `:artificer-kibbles` -> `"artificer"`). +- `traverse-nested`: Higher-order function for recursively walking nested option structures. + +### Styles (`styles/core.clj`) +- `.bg-warning`, `.bg-warning-item` CSS classes for warning banner UI. +- `.conflict-*` Garden CSS classes for conflict resolution modal (backdrop, modal, header, footer, body, radio options with color-coded variants: cyan/rename, orange/keep, purple/skip). +- `.export-issue-*` Garden CSS classes for export warning modal. + +### App State (`db.cljs`) +- Added `import-log` and `conflict-resolution` state maps to re-frame db. + +### Subscriptions (`subs.cljs`, `equipment_subs.cljs`) +- Import log, conflict resolution, export warning, missing content report subscriptions. +- `handle-api-response` HOF (`events.cljs`) — centralizes HTTP status dispatch with sensible defaults (401 → login, 500 → generic error) and catch-all logging for unhandled statuses. Replaces bare `case` statements across 7 API-calling subscriptions. + +### Entry Point (`core.cljs`) +- Dev version logging on startup. +- Import log overlay component mounted in main view wrapper. + +### Linter Configuration +- `.clj-kondo/config.edn`: Exclusions for `with-db` macro and user namespace functions. +- `.lsp/config.edn` (new): Explicit source-paths to prevent clojure-lsp from scanning compiled CLJS output in `resources/public/js/compiled/out/`. + +--- + +## Files Changed + +| Status | File | Category | +|--------|------|----------| +| Modified | `src/clj/orcpub/datomic.clj` | Error handling | +| Modified | `src/clj/orcpub/email.clj` | Error handling | +| Modified | `src/clj/orcpub/pdf.clj` | Error handling | +| Modified | `src/clj/orcpub/routes.clj` | Error handling | +| Modified | `src/clj/orcpub/routes/party.clj` | Error handling | +| Modified | `src/clj/orcpub/styles/core.clj` | UI styles | +| Modified | `src/clj/orcpub/system.clj` | Error handling | +| **New** | `src/clj/orcpub/tools/orcbrew.clj` | CLI tool | +| Modified | `src/cljc/orcpub/common.cljc` | Utilities | +| Modified | `src/cljc/orcpub/dnd/e5/options.cljc` | Bug fix | +| Modified | `src/cljc/orcpub/errors.cljc` | Error infrastructure | +| Modified | `src/cljc/orcpub/pdf_spec.cljc` | Nil guards | +| Modified | `src/cljs/orcpub/character_builder.cljs` | Warning UI | +| **New** | `src/cljs/orcpub/dnd/e5/content_reconciliation.cljs` | Missing content detection | +| Modified | `src/cljs/orcpub/dnd/e5/db.cljs` | App state | +| Modified | `src/cljs/orcpub/dnd/e5/events.cljs` | Import/export events | +| **New** | `src/cljs/orcpub/dnd/e5/import_validation.cljs` | Validation framework | +| Modified | `src/cljs/orcpub/dnd/e5/spell_subs.cljs` | Plugin robustness | +| Modified | `src/cljs/orcpub/dnd/e5/subs.cljs` | Subscriptions | +| Modified | `src/cljs/orcpub/dnd/e5/views.cljs` | Fuzzy matching, nil guards | +| **New** | `src/cljs/orcpub/dnd/e5/views/import_log.cljs` | Import log panel | +| **New** | `src/cljs/orcpub/dnd/e5/views/conflict_resolution.cljs` | Conflict/export modals | +| Modified | `web/cljs/orcpub/core.cljs` | Entry point | +| **New** | `test/clj/orcpub/errors_test.clj` | Unit tests | +| **New** | `test/cljc/orcpub/dnd/e5/favored_enemy_language_test.cljc` | Unit tests | +| **New** | `test/clj/orcpub/tools/orcbrew_test.clj` | Unit tests | +| **New** | `test/cljc/orcpub/pdf_spec_test.clj` | Unit tests | +| **New** | `test/cljs/orcpub/dnd/e5/content_reconciliation_test.cljs` | Unit tests | +| **New** | `test/cljs/orcpub/dnd/e5/import_validation_test.cljs` | Unit tests | +| Modified | `test/cljc/orcpub/dnd/e5/folder_test.clj` | Lint fix | +| **New** | `test/duplicate-external-a.orcbrew` | Test fixture | +| **New** | `test/duplicate-external-b.orcbrew` | Test fixture | +| Modified | `.clj-kondo/config.edn` | Linter config | +| **New** | `.lsp/config.edn` | LSP config | +| **New** | `docs/CONFLICT_RESOLUTION.md` | Feature documentation | +| **New** | `docs/CONTENT_RECONCILIATION.md` | Feature documentation | +| **New** | `docs/ERROR_HANDLING.md` | Feature documentation | +| **New** | `docs/HOMEBREW_REQUIRED_FIELDS.md` | Feature documentation | +| **New** | `docs/ORCBREW_FILE_VALIDATION.md` | Feature documentation | +| **New** | `docs/LANGUAGE_SELECTION_FIX.md` | Feature documentation | +| **New** | `docs/README.md` | Documentation index | +| Modified | `.gitignore` | Ignore patterns | + +--- + +## Documentation + +Feature documentation is included in `docs/`: + +| Document | Covers | +|----------|--------| +| [ERROR_HANDLING.md](docs/ERROR_HANDLING.md) | Backend error macros, error codes, usage patterns | +| [CONFLICT_RESOLUTION.md](docs/CONFLICT_RESOLUTION.md) | Duplicate key detection, resolution modal, reference updates | +| [CONTENT_RECONCILIATION.md](docs/CONTENT_RECONCILIATION.md) | Missing content detection, fuzzy matching strategies | +| [HOMEBREW_REQUIRED_FIELDS.md](docs/HOMEBREW_REQUIRED_FIELDS.md) | Required fields per content type, breaking code locations | +| [ORCBREW_FILE_VALIDATION.md](docs/ORCBREW_FILE_VALIDATION.md) | Import/export validation user and developer guide | +| [LANGUAGE_SELECTION_FIX.md](docs/LANGUAGE_SELECTION_FIX.md) | Ranger favored enemy language corruption fix (#296) | + +## Design Principles + +- **Import = permissive** (auto-fix and continue), **Export = strict** (warn user, let them decide) +- **Placeholder text convention**: `[Missing Name]` format (square brackets indicate auto-filled) +- **Modal pattern**: db state -> re-frame subscription -> event handlers -> component in `import-log-overlay` diff --git a/README.md b/README.md index fa1490774..f64254102 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,321 @@ -# orcpub +# Dungeon Master's Vault - Community Edition +
+
+ DMV +
+
-This is the code for OrcPub2.com. Many, many people have expressed interest in helping out or checking out the code, so I have decided to make that possible by open sourcing it. +
+

A web site that provides a D&D 5e Character sheet generator.

-## Getting Started with Development -- Install Java: http://openjdk.java.net/ or http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html -- Install leiningen: https://leiningen.org/ -- run `lein figwheel` +This is the code forked from OrcPub2, from the [original](https://github.com/larrychristensen/orcpub) repository on Jan 7, 2019 with improvements. -That should get a basic dev environment going and open your browser at [localhost:3449](http://localhost:3449/). -When you save changes, it will auto compile and send all changes to the browser without the -need to reload. After the compilation process is complete, you will -get a Browser Connected REPL. An easy way to try it is: +![GitHub language count](https://img.shields.io/github/languages/count/orcpub/orcpub) ![GitHub top language](https://img.shields.io/github/languages/top/orcpub/orcpub) ![GitHub contributors](https://img.shields.io/github/contributors/orcpub/orcpub) ![GitHub repo size](https://img.shields.io/github/repo-size/orcpub/orcpub) +![GitHub last commit (branch)](https://img.shields.io/github/last-commit/orcpub/orcpub/develop) -```clojure -(js/alert "Am I connected?") +![GitHub pull requests](https://img.shields.io/github/issues-pr/orcpub/orcpub) ![GitHub closed pull requests](https://img.shields.io/github/issues-pr-closed/orcpub/orcpub) + +![GitHub issues](https://img.shields.io/github/issues/orcpub/orcpub) ![GitHub closed issues](https://img.shields.io/github/issues-closed/orcpub/orcpub) + +![Docker Pulls](https://img.shields.io/docker/pulls/orcpub/orcpub) +![Docker Cloud Automated build](https://img.shields.io/docker/cloud/automated/orcpub/orcpub) +![Docker Cloud Build Status](https://img.shields.io/docker/cloud/build/orcpub/orcpub) + +[About](#about) • [Getting Started](#getting-started) • [Development](#development) • [Contributing](#how-do-i-contribute?) • [Fundamentals](#Fundamentals) + +
+ +## About +Dungeon Master's Vault is a web server that allows you to host your own Character Generator website for D&D 5th Edition. + + +## Getting Started + +To run your own install of Dungeon Master's Vault, there are two ways to do this. + +1. Pulls docker containers from our docker repository. +2. Build your own. + +In this section we will pull from the docker repository. If you want to build your own docker containers from source, see [Development](#development) + +You will need a few tools: + +- git +- A system that can run docker, with docker-compose (windows or unix) +- A SSL certificate. Self signed or from an issuing CA. +- smtp relay +- copy of this repo (for the ./deploy directory) + +### Check out this branch + + Clone a copy of our repository to your machine: + + `git clone https://github.com/Orcpub/orcpub.git` if you don't have a github account + + `git clone git@github.com:Orcpub/orcpub.git` if you do want to make changes to the code and make pull requests. + +### Quick Setup (Recommended) + +Run the automated setup script to generate secure passwords, SSL certificates, and all required directories: + +```bash +./docker-setup.sh # Interactive — prompts for each value +./docker-setup.sh --auto # Non-interactive — generates secure defaults ``` -and you should see an alert in the browser window. +Then start the containers and create your first user: + +```bash +docker-compose up -d +./docker-user.sh create admin admin@example.com MySecurePass123 +``` + +The `create` command creates a **pre-verified** account — no SMTP server or email confirmation needed. For batch user creation, additional commands, and full details see the [Docker User Management](docs/docker-user-management.md) guide. + +### Edit docker-compose.yaml + +Edit the `docker-compose.yaml` and update all the environmental variables and or paths as needed. + +The application configuration is environmental variables based, meaning that its behavior will change when modifying them at start time. + +To modify the variables edit the `docker-compose.yaml` or set your own in your shell/environment variables. + +Example environment variables: + +```shell +EMAIL_SERVER_URL: '' # DNS name of your smtp server +EMAIL_ACCESS_KEY: '' # User for the mail server +EMAIL_SECRET_KEY: '' # Password for the user +EMAIL_SERVER_PORT: 587 # Mail server port +EMAIL_FROM_ADDRESS: '' # Email address to send from, will default to 'no-reply@orcpub.com' if not set +EMAIL_ERRORS_TO: '' # Email address that errors will be sent to +EMAIL_SSL: 'false' # Should SSL be used? Gmail requires this. +EMAIL_TLS: 'false' # Should TLS be used? +DATOMIC_URL: datomic:free://datomic:4334/orcpub?password=yourpassword # Url for the database +ADMIN_PASSWORD: supersecretpassword #The datomic admin password (should be different than the DATOMIC_PASSWORD) +DATOMIC_PASSWORD: yourpassword #The datomic application password +SIGNATURE: '' # The Secret used to hash your password in the browser, 20+ characters recommended +``` + +The `ADMIN_PASSWORD` and `DATOMIC_PASSWORD` -Before you start up the back-end server, you will need to [set up Datomic locally](https://docs.datomic.com/on-prem/dev-setup.html). If you're just trying to get started quickly to contribute to the main project, and happen to be on macOS, you can use [homebrew](https://brew.sh/) to do this pretty quickly: +Update the `` in the `DATOMIC_URL` to match the password used in `DATOMIC_PASSWORD`. +Create an SSL certificate using `deploy/snakeoil.sh (or bat)` or simply edit the paths to an existing SSL certificate and key in the `web` service definition. +These passwords are used to secure the database server Datomic. + +### Create a certificate or use an existing one + +You will need a webserver certificate. For a quick SSL certificate, the script at `./deploy/snakeoil.sh` (unix) or `./deploy/snakeoil.bat` (windows) will create self signed certificate you can use, or you can make a request to a CA and install one from there. + +By default the certificate is named `snakeoil.crt` and `snakeoil.key` and used by the nginx container here: + +```shell + volumes: + - ./deploy/nginx.conf:/etc/nginx/conf.d/default.conf + - ./deploy/snakeoil.crt:/etc/nginx/snakeoil.crt + - ./deploy/snakeoil.key:/etc/nginx/snakeoil.key + - ./deploy/homebrew/:/usr/share/nginx/html/homebrew/ ``` -brew install datomic -brew services start datomic + +For windows you will need OpenSSL installed to run the `./deploy/snakeoil.bat`. + +OpenSSL be installed via [chocolatey](https://chocolatey.org/) `choco install openssl` + +### nginx.conf + +You will need the `./deploy/nginx.conf` or roll your own. + +### Launch the docker containers + +```shell + docker-compose pull + docker-compose up + -or- + docker-compose up -d ``` -You will then need to transact the schema. First start a REPL: +If all went well you should be able to hit the site via `https://localhost` + +If not - run the docker containers with `docker-compose up` which will show you the logs of the containers and troubleshoot from there. + +### Importing your orcbrews automatically + +To have your orcbrew file you want to load automatically when a new client connects, place it in the `./deploy/homebrew/homebrew.orcbrew` + +All orcbrew files have to be combined into a single file named "homebrew.orcbrew". + +### Character Data + +**Data directory** + +Character item data is held in a database provided by Datomic. Datomic stores the character and magic item information in the `./data` directory. + +If you want to backup the database you only need to copy the `./data` directory after Datomic is shutdown. + +If you want a new database, delete the `./data` directory to start over. + +**Log directory** + +The `./logs` directory contains error logs for Datomic itself and any files here can be safely removed with out affecting character data. + +Watch this directory and clean up old files, it can grow quite large quickly. It is recommended to setup log rotate or some other mechanism to clean these up. + +## Development + +### Building your own docker images + +There are three docker containers that will be built. + +- orcpub_datomic_1 - the database service. +- orcpub_orcpub_1 - the JRE service that is the website. +- orcpub_web_1 - the ngnix web server that reverse proxies back to the JRE service. + +**Dependencies** + +- [Docker](https://docs.docker.com/install/) +- [Docker Compose](https://docs.docker.com/compose/) +- [git](https://git-scm.com/downloads) + + +Unix instructions [here](https://github.com/Orcpub/orcpub/wiki/Orcpub-on-Ubuntu-18.04-with-Docker) + +Windows instructions [here](https://github.com/Orcpub/orcpub/wiki/Orcpub-on-Windows-10-with-Docker) + +Docker Cheat [Sheet](https://github.com/Orcpub/orcpub/wiki/Docker-Cheat-sheet) + +### Getting started - building the docker image from source + +There are two docker-compose example files in this repository. + +`docker-compose.yaml` will pull from the docker repo which the community maintains and is rebuilt with the latest code from the develop branch. **this is the default** + +`docker-compose-build.yaml` is an example of how to build from the local source from a git clone. + +Rename docker-compose-build.yaml to docker-compose.yaml and it will build from your downloaded cloned directory. + +1. Start by forking this repo in your own github account and checkout the **develop** branch. `git clone git@github.com:Orcpub/orcpub.git` +2. Create snakeoil (self-signed) ssl certificates by running `./deploy/snakeoil.sh | .bat` or modify the docker-compose.yaml to your certificates. +3. Modify the docker-compose.yaml and code you want to. +4. Run `docker-compose build` to create the new containers built from the source. +5. Run docker-compose `docker-compose up` or if you want to demonize it `docker-compose up -d` +6. The website should be accessible via browser in `https://localhost` + +**NOTE** + +The application configuration is Environmental Variable based, meaning that its behavior will change when modifying them at start time. To modify the variables edit the `docker-compose.yaml` or `docker-compose-build.yaml` or set your own in your shell/environment. + +Example variables: + +```shell +EMAIL_SERVER_URL: '' # Url to a smtp server +EMAIL_ACCESS_KEY: '' # User for the mail server +EMAIL_SECRET_KEY: '' # Password for the user +EMAIL_SERVER_PORT: 587 # Mail server port +EMAIL_FROM_ADDRESS: '' # Email address to send from, will default to 'no-reply@orcpub.com' +EMAIL_ERRORS_TO: '' # Email address that errors will be sent to +EMAIL_SSL: 'false' # Should SSL be used? Gmail requires this. +DATOMIC_URL: datomic:free://datomic:4334/orcpub?password=yourpassword # Url for the database +ADMIN_PASSWORD: supersecretpassword +DATOMIC_PASSWORD: yourpassword #(Same as above) +SIGNATURE: '' # The Secret used to hash your password in the browser, 20+ characters recommended ``` -lein repl + +To change the datomic passwords you can do it through the environment variables `ADMIN_PASSWORD_OLD` and `DATOMIC_PASSWORD_OLD` start the container once, then set the `ADMIN_PASSWORD` and `DATOMIC_PASSWORD` to your new passwords. + +More on these passwords here. +[ADMIN_PASSWORD](https://docs.datomic.com/on-prem/configuring-embedded.html#sec-2-1) +[DATOMIC_PASSWORD](https://docs.datomic.com/on-prem/configuring-embedded.html#sec-2-1) + +## How do I contribute? +Thank you for rolling for initiative! + +We work on forks, and branches. Fork our repo, then create a new branch for any bug or new feature that you want to work on. + +### Get started + +- Install Java: http://openjdk.java.net/ +- or http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html +- For MacOS/Linux Download [Datomic](https://www.datomic.com/get-datomic.html), and unzip it into a directory. + +* For Windows [DMV Datomic](https://github.com/Orcpub/orcpub/raw/refs/heads/develop/lib/datomic-free-0.9.5703.tar.gz) - newer versions do not work on Windows. it's a known issue that the Datomic team hasn't bothered to solve. It has to do with the max characters that windows can hold for a path. + + Launch Datomic by going to shell/cmd prompt in the unzipped directory and run: + + On Windows: + + `bin\transactor config/samples/free-transactor-template.properties` + + On Mac/Unix: + + `bin/transactor config/samples/free-transactor-template.properties` + + +- Install [leiningen](https://leiningen.org/#install) + - Mac / Linux: The latest version (2.9.1 as of this writing) should work. + - Windows: 2.9.3 Can be installed with [chocolatey](https://chocolatey.org/install) using `choco install lein --version 2.9.3` + +- Download the code from your git fork + + `git clone git@github.com:yourrepo/your.git` + + Use the clone url in YOUR repo. + +- cd into orcpub +- create a new branch for the bug fix or feature you are about to work on `git checkout -b ` +- Pick an editor from the next steps. +- run `lein with-profile +start-server repl` +- run `lein figwheel` Once lein figwheel finishes, a browser will launch. + +You should have all three processes running: the Datomic transactor, lein repl, and lein figwheel. + +On the front end, When you save changes, it will auto compile and send all changes to the browser without the +need to reload. After the compilation process is complete, you will get a Browser Connected REPL. + +An easy way to try it is: + +```clojure +(js/alert "Am I connected?") ``` -Or if you are using Emacs with [Cider](https://cider.readthedocs.io/en/latest/) you can run the command to start the Cider REPL: +and you should see an alert in the browser window. +On the backend (PDF generation) you will have to restart lein repl to get your changes. + +Code away! and make your commits. + +When your branch is ready create a pull request on our repo for a code review and merge back into our branch. + + +### Suggested Editors + +### Emacs +Emacs with [Cider](https://cider.readthedocs.io/en/latest/) you can run the command to start the Cider REPL: ``` C-c M-j ``` -For Vim users, [vim-fireplace](https://github.com/tpope/vim-fireplace) provides a good way to interact with a running repl without leaving Vim. +### Vim +[vim-fireplace](https://github.com/tpope/vim-fireplace) provides a good way to interact with a running repl without leaving Vim. + +### IntelliJ / Cursive +You can use the community edition of [IntelliJ IDEA](https://www.jetbrains.com/idea/download/) with the [Cursive plug-in](https://cursive-ide.com/). + +### VS Code +You can use the open source edition of [Visual Studio Code](https://code.visualstudio.com/Download) with the Calva: Clojure & ClojureScript Interactive Programming, Clojure Code, and Bookmarks Extensions. + +To start REPL with VS Code: +* first launch datomic in a cmd window with the transactor snippet from above: `bin\transactor config/samples/free-transactor-template.properties` + * you can also just add that to a `.ps1` file inside your project for easier reference eg. `run-datomic ps1` +* THEN jack-in using the `Leiningen + Legacy Figwheel`, `figwheel-native`, and select the `:dev` and optionally `:start-server` + +### REPL -I haven't used [Cursive](https://cursive-ide.com/), but I hear it is really nice and I'm sure there's an easy way to start a REPL within it. +Once you have a REPL, you can run this from within it to create the database, transact the database schema, and start the server: -Once you have a REPL you can run this from within it to create the database, transact the database schema, and start the server: +You only need to `(init-database)` ONCE. ```clojure user=> (init-database) @@ -57,11 +330,17 @@ user=> (stop-server) Within Emacs you should be able to save your file (C-x C-s) and reload it into the REPL (C-c C-w) to get your server-side changes to take effect. Within Vim with `vim-fireplace` you can eval a form with `cpp`, a paragraph with `cpip`, etc; check out its help file for more information. Regardless of editor, your client-side changes will take effect immediately when you change a CLJS or CLJC file while `lein figwheel` is running. -## OrcPub Fundamentals +## Fundamentals -### Overview +### Overview - from the original author - Larry -OrcPub's design is based around the concept of hierarchical option selections applying modifiers to a entity. Consider D&D 5e as an example. In D&D 5e you build and maintain characters, which are entities, by selecting from a set of character options, such as race and class. When you select a race you will be affored other option selections, such as subrace or subclass. Option selections also apply modifiers to your character, such as 'Darkvision 60'. Option selections are defined in templates. An entity is really just a record of hierarchical choices made. A built entity is a collection of derived attributes and functions derived from applying all of the modifiers of all the choices made. Here is some pseudocode to this more concrete: +The design is based around the concept of hierarchical option selections applying modifiers to a entity. + +Consider D&D 5e as an example. In D&D 5e you build and maintain characters, which are entities, by selecting from a set of character options, such as race and class. When you select a race you will be afforded other option selections, such as subrace or subclass. + +Option selections also apply modifiers to your character, such as 'Darkvision 60'. Option selections are defined in templates. An entity is really just a record of hierarchical choices made. + +A built entity is a collection of derived attributes and functions derived from applying all of the modifiers of all the choices made. Here is some pseudocode to this more concrete: ```clojure user> (def character-entity {:options {:race @@ -92,14 +371,16 @@ user> built-character :int-bonus 1} ``` -This may seem overly complicated, but after my work on the [Original Orcpub.com](orcpub.com), I realized that this really the only real correct solution as it models how character building actually works. The original Orcpub stored characters essentially like the built-character above with a centralized set of functions to compute other derived values. This is the most straightforward solution, but this has flaws: +This may seem overly complicated, but after my work on the Original Orcpub.com, I realized that this really the only real correct solution as it models how character building actually works. + +The original Orcpub stored characters essentially like the built-character above with a centralized set of functions to compute other derived values. This is the most straightforward solution, but this has flaws: * You have difficulty figuring out which options have been selected and which ones still need to be selected. * You keep having to patch your data as your application evolves. For example, say you store a character's known spells as a list of spell IDs. Then you realize later that users want to also know what their attack bonus is for each spell. At the very least you'll have to make some significant changes to every stored character. * It is not scalable. Every time you add in more options, say from some new sourcebook, you have to pile on more conditional logic in your derived attribute functions. Believe me, this gets unmanageable very quickly. * It's not reusable in, say, a Rifts character builder. -The OrcPub2 architecture fixes these problems: +The architecture fixes these problems: * You know exactly which options have been selected, which have not, and how every modifier is arrived at, given the entity and the most up-to-date templates. * You don't need to patch up stored characters if you find a bug since characters are stored as just a set of very generic choices. @@ -141,9 +422,13 @@ Given this, you might start calculating initiative as follows: (ability-modifier (dexterity character))) ``` -Consider what happens when you need to account for the 'Improved Initiative' feat, you'll need to add the calculation to the initiative function. Okay, this is probably still manageable. Then consider what happens when some cool subclass comes along that gets an initiative bonus at 9th level. Now it starts getting more unmanagable. When you try to add every option from every book into the mix, each of which might have some totally new condition for modifying initiative, you quickly end up with a nausiating ball of mud that will be scary to work with. +Consider what happens when you need to account for the 'Improved Initiative' feat, you'll need to add the calculation to the initiative function. Okay, this is probably still manageable. -OrcPub2 decentralizes most calculations using modifiers associated with selected character options. When you add options you also specify any modifiers associated with that option. For example, in the OrcPub2 entity example above, we have the elf option: +Then consider what happens when some cool subclass comes along that gets an initiative bonus at 9th level. Now it starts getting more unmanageable. When you try to add every option from every book into the mix, each of which might have some totally new condition for modifying initiative, you quickly end up with a nauseating ball of mud that will be scary to work with. + +This method decentralizes most calculations using modifiers associated with selected character options. When you add options you also specify any modifiers associated with that option. + +For example, in the entity example above, we have the elf option: ```clojure {:name "Elf" @@ -153,7 +438,17 @@ OrcPub2 decentralizes most calculations using modifiers associated with selected ...} ``` -If you build a character that has this :elf option selected, the modifiers will be applied the the :dex-bonus and :race in the built character. Let's look closer at the ?dex-bonus modifier. The second argument to the modifier function is a special symbol that prefixes a ? on the attribute instead of the : we'll expect on the output attribute key, in this case ?dex-bonus will be modifying the value output to the :dex-bonus attribute. The third argument is a modifier body. This can be any Clojure expression you like, but if you will be deriving your new value from an old value or from another attribute you must use the ? reference. In this example we updating ?dex-bonus by adding 2 to it. Modifiers can be derived from attributes that are derived from other attributes, and so forth. For example, we may have a character whose options provide the following chain of modifiers: +If you build a character that has this :elf option selected, the modifiers will be applied the the :dex-bonus and :race in the built character. Let's look closer at the ?dex-bonus modifier. + +The second argument to the modifier function is a special symbol that prefixes a ? on the attribute instead of the : we'll expect on the output attribute key, in this case ?dex-bonus will be modifying the value output to the :dex-bonus attribute. + +The third argument is a modifier body. + +This can be any Clojure expression you like, but if you will be deriving your new value from an old value or from another attribute you must use the ? reference. In this example we updating ?dex-bonus by adding 2 to it. + +Modifiers can be derived from attributes that are derived from other attributes, and so forth. + +For example, we may have a character whose options provide the following chain of modifiers: ```clojure (modifier ?dexterity 12) @@ -164,7 +459,7 @@ If you build a character that has this :elf option selected, the modifiers will (modifier ?initiative (+ ?initiative (* 2 ?intelligence-mod))) ``` -#### Modifier Order is Important! +### Modifier Order is Important! Consider what would happen if we applied the above modifiers in a different order: @@ -176,34 +471,26 @@ Consider what would happen if we applied the above modifiers in a different orde (modifier ?int-mod (ability-mod ?intelligence)) (modifier ?initiative ?dex-mod) ``` -Either our initiative calculation would throw an error our it would be completely wrong since the other derived attributes it depends on have not been applied yet. There is no logical ordering for which options should be applied, so modifiers can very well be provided out of order. For this reason we have to build a dependency graph of derived attributes and then apply the modifiers in topologically sorted order. Identifying these dependencies is why we use the ? references. - -## FAQs -**Q: Ummmmm, why is your code so ugly, I thought Clojure code was supposed to be pretty.** - -**A:** *Yeah, about that...I worked on this for about 4 months full time, trying to compete with D&D Beyond's huge team and budget. That lead to a stressed-out me and ugly code. Help me make it pretty!* - - -**Q: Mwahahahaha, now that I have your code I'm going to fork it and build the most awesome website in the world that will totally frackin' annihilate OrcPub2.com. I'm going to call it FlumphTavern69.com. Come at me bro!** +Either our initiative calculation would throw an error our it would be completely wrong since the other derived attributes it depends on have not been applied yet. There is no logical ordering for which options should be applied, so modifiers can very well be provided out of order. -**A:** *Hell yeah, do that, flumphs are some sexy creatures!* - - -**Q: Blahahahaha, you done fracked up, we are super-mega-corp Hex Inc. we will steal your awesome code and put it into our less awesome app. What you got to say about that, huh, beotch?** - -**A:** *I'm down for that, your app makes me sad, if you were to combine your official license and professional visual design with my more modern technical and UX design, your app would make me happy and I could justify paying all the money for all the content* +For this reason we have to build a dependency graph of derived attributes and then apply the modifiers in topologically sorted order. Identifying these dependencies is why we use the ? references. +## FAQs +**Q: I'm a newb Clojure developer looking to get my feet wet, where to start?** -**Q: Seriously?!!! Your unit test coverage is pathetic!** +**A:** *First I would start by getting the fundamentals down at https://4clojure.oxal.org/ From there you might add some unit tests or pick up an open issue on the "Issues" tab (and add unit tests with it).* -**A:** *Yep, add some, it would be awesome.* +**Q: Your DSL for defining character options is pretty cool, I can build any type of character option out there. How about I add a bunch on content from the Player's Handbook?** -**Q: I'm a newb Clojure developer looking to get my feet wet, where to start?** +**A:** *We love your enthusiasm, but we cannot accept pull requests containing copyrighted content. We do, however, encourage you to fork us and create your own private version with the full content options.* -**A:** *First I would start by getting the fundamentals down at http://www.4clojure.com/, then maybe getting your bearing by checking out my more gentle (and clean) introduction to the OrcPub stack: https://github.com/larrychristensen/messenjer, which I walkthrough on https://lambdastew.com. From there you might add some unit tests or pick up an open issue on the "Issues" tab (and add unit tests with it).* +## Disclaimer +The use of this tool is meant for use for your own use and your own content. It is only meant and should only be used on campaigns with content that you legally possess. This tool is not affiliated with Roll20, or Wizards of the Coast. -**Q: Your DSL for defining character options is pretty cool, I can build any type of character option out there. How about I add a bunch on content from the Player's Handbook?** +## Credits +Larry Christensen original author of [Orcpub2](https://github.com/larrychristensen/orcpub) -**A:** *I love your enthusiasm, but we cannot accept pull requests containing copyrighted content. We do, however, encourage you to fork OrcPub and create your own private version with the full content options* +## License +[EPL-2.0](LICENSE) \ No newline at end of file diff --git a/bitbucket-pipelines.yml b/bitbucket-pipelines.yml deleted file mode 100644 index 86e2cb9fb..000000000 --- a/bitbucket-pipelines.yml +++ /dev/null @@ -1,7 +0,0 @@ -image: clojure:lein-2.6.1 - -pipelines: - default: - - step: - script: - - lein test \ No newline at end of file diff --git a/deploy/homebrew/readme b/deploy/homebrew/readme new file mode 100644 index 000000000..a367facbf --- /dev/null +++ b/deploy/homebrew/readme @@ -0,0 +1,4 @@ +To have your homebrew orcbrew file automatically loaded when a new client connects place it here with the name "homebrew.orcbrew". +All orcbrew files have to be combined into a single file named "homebrew.orcbrew" because that is the only file that will be loaded. +This will elect to not import the homebrew if any other homebrew has already been imported on that device. +If you want to overwrite what is already loaded delete all of the content from the Content List section and reload the page. \ No newline at end of file diff --git a/deploy/nginx.conf b/deploy/nginx.conf new file mode 100644 index 000000000..22f4bfa85 --- /dev/null +++ b/deploy/nginx.conf @@ -0,0 +1,57 @@ +server { + listen 80 default_server; + server_name _; + return 301 https://$host$request_uri; +} + +server { + listen 443 ssl; + server_name _; + + ssl_certificate /etc/nginx/snakeoil.crt; + ssl_certificate_key /etc/nginx/snakeoil.key; + #charset koi8-r; + #access_log /var/log/nginx/host.access.log main; + + location / { + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Real-IP $remote_addr; + proxy_pass http://orcpub:8890; + } + + #error_page 404 /404.html; + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /usr/share/nginx/html; + } + location /homebrew.orcbrew { + root /usr/share/nginx/html/homebrew; + } + + # proxy the PHP scripts to Apache listening on 127.0.0.1:80 + # + #location ~ \.php$ { + # proxy_pass http://127.0.0.1; + #} + + # pass the PHP scripts to FastCGI server listening on 127.0.0.1:9000 + # + #location ~ \.php$ { + # root html; + # fastcgi_pass 127.0.0.1:9000; + # fastcgi_index index.php; + # fastcgi_param SCRIPT_FILENAME /scripts$fastcgi_script_name; + # include fastcgi_params; + #} + + # deny access to .htaccess files, if Apache's document root + # concurs with nginx's one + # + #location ~ /\.ht { + # deny all; + #} +} diff --git a/deploy/snakeoil.bat b/deploy/snakeoil.bat new file mode 100644 index 000000000..c56ab222d --- /dev/null +++ b/deploy/snakeoil.bat @@ -0,0 +1 @@ +openssl req -subj "/C=PL/ST=Warsaw/L=Warsaw/O=Orcpub Web/OU=Orcpub/CN=*" -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout snakeoil.key -out snakeoil.crt diff --git a/deploy/snakeoil.sh b/deploy/snakeoil.sh new file mode 100755 index 000000000..d5084f727 --- /dev/null +++ b/deploy/snakeoil.sh @@ -0,0 +1,10 @@ +#!/bin/bash +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +openssl req \ + -subj "/C=PL/ST=Warsaw/L=Warsaw/O=Orcpub Web/OU=Orcpub/CN=${PWD##*/}" \ + -x509 \ + -nodes \ + -days 365 \ + -newkey rsa:2048 \ + -keyout "${DIR}/snakeoil.key" \ + -out "${DIR}/snakeoil.crt" diff --git a/deploy/start.sh b/deploy/start.sh new file mode 100644 index 000000000..538b1dc5a --- /dev/null +++ b/deploy/start.sh @@ -0,0 +1,27 @@ +#!/usr/bin/env bash + +if [ -z "$ADMIN_PASSWORD" ]; then + echo "Environment variable ADMIN_PASSWORD not set. See https://docs.datomic.com/on-prem/configuring-embedded.html#sec-2-1" + exit 1 +fi + +if [ -z "$DATOMIC_PASSWORD" ]; then + echo "Environment variable DATOMIC_PASSWORD not set. See https://docs.datomic.com/on-prem/configuring-embedded.html#sec-2-1" + exit 1 +fi + +sed "/host=datomic/a alt-host=${ALT_HOST:-127.0.0.1}" -i /datomic/transactor.properties +sed "s/# storage-admin-password=/storage-admin-password=${ADMIN_PASSWORD}/" -i /datomic/transactor.properties +sed "s/# storage-datomic-password=/storage-datomic-password=${DATOMIC_PASSWORD}/" -i /datomic/transactor.properties + +if [ -n "$ADMIN_PASSWORD_OLD" ]; then + sed "s/# old-storage-admin-password=/old-storage-admin-password=$ADMIN_PASSWORD_OLD/" -i /datomic/transactor.properties +fi + +if [ -n "$DATOMIC_PASSWORD_OLD" ]; then + sed "s/# old-storage-datomic-password=/old-storage-datomic-password=$DATOMIC_PASSWORD_OLD/" -i /datomic/transactor.properties +fi + +sed "s/# encrypt-channel=true/encrypt-channel=${ENCRYPT_CHANNEL:-true}/" -i /datomic/transactor.properties + +/datomic/bin/transactor /datomic/transactor.properties diff --git a/dev/user.clj b/dev/user.clj index 20848d71d..694b22b19 100644 --- a/dev/user.clj +++ b/dev/user.clj @@ -1,11 +1,14 @@ (ns user - (:require [figwheel-sidecar.repl-api :as f] + (:require [clojure.java.io :as io] [com.stuartsierra.component :as component] + [figwheel-sidecar.repl-api :as f] [datomic.api :as datomic] [orcpub.routes :as r] [orcpub.system :as s] [orcpub.db.schema :as schema])) +(alter-var-root #'*print-length* (constantly 50)) + ;; user is a namespace that the Clojure runtime looks for and ;; loads if its available @@ -45,11 +48,18 @@ ;; nothing in -server: (throw (IllegalStateException. "Call (start-server) first")))) -(defn get-cljs-builds +(defn- project-form + [] + (with-open [r (java.io.PushbackReader. (io/reader "project.clj"))] + (binding [*read-eval* false] + (loop [form (read r)] + (if (= (first form) 'defproject) + form + (recur (read r))))))) + +(defn get-cljs-build [id] - (let [project-config (->> "project.clj" - slurp - read-string + (let [project-config (->> (project-form) (drop 1) (apply hash-map)) build (->> project-config @@ -95,11 +105,13 @@ :db db})))) (defn fig-start - "This starts the figwheel server and watch based auto-compiler." + "This starts the figwheel server and watch based auto-compiler. + + Afterwards, call (cljs-repl) to connect." ([] (fig-start "dev")) ([build-id] - ;; this call will only work are long as your :cljsbuild and + ;; this call will only work as long as your :cljsbuild and ;; :figwheel configurations are at the top level of your project.clj ;; and are not spread across different lein profiles @@ -107,7 +119,7 @@ (f/start-figwheel! {:figwheel-options {} :build-ids [build-id] - :all-builds (get-cljs-builds build-id)}))) + :all-builds (get-cljs-build build-id)}))) (defn fig-stop "Stop the figwheel server and watch based auto-compiler." @@ -117,6 +129,8 @@ ;; if you are in an nREPL environment you will need to make sure you ;; have setup piggieback for this to work (defn cljs-repl - "Launch a ClojureScript REPL that is connected to your build and host environment." + "Launch a ClojureScript REPL that is connected to your build and host environment. + + (NB: Call fig-start first.)" [] (f/cljs-repl)) diff --git a/docker-compose-build.yaml b/docker-compose-build.yaml new file mode 100644 index 000000000..015b6daa8 --- /dev/null +++ b/docker-compose-build.yaml @@ -0,0 +1,58 @@ +--- +services: + orcpub: + build: + context: . + dockerfile: docker/orcpub/Dockerfile + environment: + PORT: ${PORT:-8890} + EMAIL_SERVER_URL: ${EMAIL_SERVER_URL:-} + EMAIL_ACCESS_KEY: ${EMAIL_ACCESS_KEY:-} + EMAIL_SECRET_KEY: ${EMAIL_SECRET_KEY:-} + EMAIL_SERVER_PORT: ${EMAIL_SERVER_PORT:-587} + EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-} + EMAIL_ERRORS_TO: ${EMAIL_ERRORS_TO:-} + EMAIL_SSL: ${EMAIL_SSL:-FALSE} + EMAIL_TLS: ${EMAIL_TLS:-FALSE} + DATOMIC_URL: ${DATOMIC_URL:-datomic:free://datomic:4334/orcpub?password=change-me} + SIGNATURE: ${SIGNATURE:-change-me-to-something-unique} + depends_on: + datomic: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8890/"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 15s + restart: always + datomic: + build: + context: . + dockerfile: docker/datomic/Dockerfile + environment: + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-me-admin} + DATOMIC_PASSWORD: ${DATOMIC_PASSWORD:-change-me} + volumes: + - ./data:/data + - ./logs:/logs + healthcheck: + test: ["CMD-SHELL", "grep -q ':10EE ' /proc/net/tcp || grep -q ':10EE ' /proc/net/tcp6"] + interval: 5s + timeout: 3s + retries: 30 + start_period: 40s + restart: always + web: + image: nginx:alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./deploy/nginx.conf:/etc/nginx/conf.d/default.conf + - ./deploy/snakeoil.crt:/etc/nginx/snakeoil.crt + - ./deploy/snakeoil.key:/etc/nginx/snakeoil.key + depends_on: + orcpub: + condition: service_healthy + restart: always diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 000000000..08e508977 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,55 @@ +--- +services: + orcpub: + image: orcpub/orcpub:latest + environment: + PORT: ${PORT:-8890} + EMAIL_SERVER_URL: ${EMAIL_SERVER_URL:-} + EMAIL_ACCESS_KEY: ${EMAIL_ACCESS_KEY:-} + EMAIL_SECRET_KEY: ${EMAIL_SECRET_KEY:-} + EMAIL_SERVER_PORT: ${EMAIL_SERVER_PORT:-587} + EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-} + EMAIL_ERRORS_TO: ${EMAIL_ERRORS_TO:-} + EMAIL_SSL: ${EMAIL_SSL:-FALSE} + EMAIL_TLS: ${EMAIL_TLS:-FALSE} + DATOMIC_URL: ${DATOMIC_URL:-datomic:free://datomic:4334/orcpub?password=change-me} + SIGNATURE: ${SIGNATURE:-change-me-to-something-unique} + depends_on: + datomic: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8890/"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 15s + restart: always + datomic: + image: orcpub/datomic:latest + environment: + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-me-admin} + DATOMIC_PASSWORD: ${DATOMIC_PASSWORD:-change-me} + volumes: + - ./data:/data + - ./logs:/logs + healthcheck: + test: ["CMD-SHELL", "grep -q ':10EE ' /proc/net/tcp || grep -q ':10EE ' /proc/net/tcp6"] + interval: 5s + timeout: 3s + retries: 30 + start_period: 40s + restart: always + web: + image: nginx:alpine + ports: + - "80:80" + - "443:443" + volumes: + - ./deploy/nginx.conf:/etc/nginx/conf.d/default.conf + - ./deploy/snakeoil.crt:/etc/nginx/snakeoil.crt + - ./deploy/snakeoil.key:/etc/nginx/snakeoil.key + - ./deploy/homebrew/:/usr/share/nginx/html/homebrew/ + depends_on: + orcpub: + condition: service_healthy + restart: always diff --git a/docker-setup.sh b/docker-setup.sh new file mode 100755 index 000000000..6d97a64fa --- /dev/null +++ b/docker-setup.sh @@ -0,0 +1,348 @@ +#!/usr/bin/env bash +# +# OrcPub / Dungeon Master's Vault — Docker Setup Script +# +# Prepares everything needed to run the application via Docker Compose: +# 1. Generates secure random passwords and a signing secret +# 2. Creates a .env file (or uses an existing one) +# 3. Generates self-signed SSL certificates (if missing) +# 4. Creates required directories (data, logs, deploy/homebrew) +# +# Usage: +# ./docker-setup.sh # Interactive mode — prompts for optional values +# ./docker-setup.sh --auto # Non-interactive — accepts all defaults +# ./docker-setup.sh --help # Show usage +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="${SCRIPT_DIR}/.env" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +color_green='\033[0;32m' +color_yellow='\033[1;33m' +color_red='\033[0;31m' +color_cyan='\033[0;36m' +color_reset='\033[0m' + +info() { printf '%s[INFO]%s %s\n' "$color_green" "$color_reset" "$*"; } +warn() { printf '%s[WARN]%s %s\n' "$color_yellow" "$color_reset" "$*"; } +error() { printf '%s[ERROR]%s %s\n' "$color_red" "$color_reset" "$*" >&2; } +header() { printf '\n%s=== %s ===%s\n\n' "$color_cyan" "$*" "$color_reset"; } + +generate_password() { + # Generate a URL-safe random password (no special chars that break URLs/YAML) + local length="${1:-24}" + if command -v openssl &>/dev/null; then + openssl rand -base64 "$length" | tr -d '/+=' | head -c "$length" + elif [ -r /dev/urandom ]; then + tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$length" + else + error "Cannot generate random password: no openssl or /dev/urandom available" + exit 1 + fi +} + +prompt_value() { + local prompt_text="$1" + local default_value="$2" + local result + + if [ "${AUTO_MODE:-false}" = "true" ]; then + echo "$default_value" + return + fi + + if [ -n "$default_value" ]; then + read -rp "${prompt_text} [${default_value}]: " result + echo "${result:-$default_value}" + else + read -rp "${prompt_text}: " result + echo "$result" + fi +} + +usage() { + cat <<'USAGE' +Usage: ./docker-setup.sh [OPTIONS] + +Options: + --auto Non-interactive mode; accept all defaults + --force Overwrite existing .env file + --help Show this help message + +Examples: + ./docker-setup.sh # Interactive setup + ./docker-setup.sh --auto # Quick setup with generated defaults + ./docker-setup.sh --auto --force # Regenerate everything from scratch +USAGE +} + +# --------------------------------------------------------------------------- +# Parse arguments +# --------------------------------------------------------------------------- + +AUTO_MODE=false +FORCE_MODE=false + +for arg in "$@"; do + case "$arg" in + --auto) AUTO_MODE=true ;; + --force) FORCE_MODE=true ;; + --help) usage; exit 0 ;; + *) + error "Unknown option: $arg" + usage + exit 1 + ;; + esac +done + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +header "Dungeon Master's Vault — Docker Setup" + +# ---- Step 1: .env file --------------------------------------------------- + +if [ -f "$ENV_FILE" ] && [ "$FORCE_MODE" = "false" ]; then + info "Existing .env file found. Skipping generation (use --force to overwrite)." +else + # Source existing .env (if any) so current values become defaults for prompts + if [ -f "$ENV_FILE" ]; then + # shellcheck disable=SC1090 + . "$ENV_FILE" + fi + + header "Database Passwords" + + # Generate defaults but let user override + DEFAULT_ADMIN_PW="$(generate_password 24)" + DEFAULT_DATOMIC_PW="$(generate_password 24)" + DEFAULT_SIGNATURE="$(generate_password 32)" + + ADMIN_PASSWORD=$(prompt_value "Datomic admin password" "$DEFAULT_ADMIN_PW") + DATOMIC_PASSWORD=$(prompt_value "Datomic application password" "$DEFAULT_DATOMIC_PW") + SIGNATURE=$(prompt_value "JWT signing secret (20+ chars)" "$DEFAULT_SIGNATURE") + + header "Application" + + PORT=$(prompt_value "Application port" "8890") + EMAIL_SERVER_URL=$(prompt_value "SMTP server URL (leave empty to skip email)" "") + EMAIL_ACCESS_KEY="" + EMAIL_SECRET_KEY="" + EMAIL_SERVER_PORT="587" + EMAIL_FROM_ADDRESS="" + EMAIL_ERRORS_TO="" + EMAIL_SSL="FALSE" + EMAIL_TLS="FALSE" + + if [ -n "$EMAIL_SERVER_URL" ]; then + EMAIL_ACCESS_KEY=$(prompt_value "SMTP username" "") + EMAIL_SECRET_KEY=$(prompt_value "SMTP password" "") + EMAIL_SERVER_PORT=$(prompt_value "SMTP port" "587") + EMAIL_FROM_ADDRESS=$(prompt_value "From email address" "no-reply@orcpub.com") + EMAIL_ERRORS_TO=$(prompt_value "Error notification email" "") + EMAIL_SSL=$(prompt_value "Use SSL? (TRUE/FALSE)" "FALSE") + EMAIL_TLS=$(prompt_value "Use TLS? (TRUE/FALSE)" "FALSE") + fi + + header "Initial Admin User" + + # Check environment / existing .env for pre-set values + INIT_ADMIN_USER="${INIT_ADMIN_USER:-}" + INIT_ADMIN_EMAIL="${INIT_ADMIN_EMAIL:-}" + INIT_ADMIN_PASSWORD="${INIT_ADMIN_PASSWORD:-}" + + if [ -n "$INIT_ADMIN_USER" ] && [ -n "$INIT_ADMIN_EMAIL" ] && [ -n "$INIT_ADMIN_PASSWORD" ]; then + info "Using admin user from environment: ${INIT_ADMIN_USER} <${INIT_ADMIN_EMAIL}>" + elif [ "${AUTO_MODE}" = "true" ]; then + info "No INIT_ADMIN_* variables set. Skipping admin user setup." + info "Create users later with: ./docker-user.sh create ..." + else + info "Optionally create an initial admin account." + info "You can skip this and create users later with ./docker-user.sh" + echo "" + INIT_ADMIN_USER=$(prompt_value "Admin username (leave empty to skip)" "") + if [ -n "$INIT_ADMIN_USER" ]; then + INIT_ADMIN_EMAIL=$(prompt_value "Admin email" "") + INIT_ADMIN_PASSWORD=$(prompt_value "Admin password" "") + if [ -z "$INIT_ADMIN_EMAIL" ] || [ -z "$INIT_ADMIN_PASSWORD" ]; then + warn "Email and password are required. Skipping admin user setup." + INIT_ADMIN_USER="" + INIT_ADMIN_EMAIL="" + INIT_ADMIN_PASSWORD="" + fi + fi + fi + + info "Writing .env file..." + + cat > "$ENV_FILE" </dev/null; then + info "Generating self-signed SSL certificate..." + openssl req \ + -subj "/C=US/ST=State/L=City/O=OrcPub/OU=Dev/CN=localhost" \ + -x509 \ + -nodes \ + -days 365 \ + -newkey rsa:2048 \ + -keyout "$KEY_FILE" \ + -out "$CERT_FILE" \ + 2>/dev/null + info "SSL certificate generated (valid for 365 days)." + else + warn "openssl not found — cannot generate SSL certificates." + warn "Install openssl and run: ./deploy/snakeoil.sh" + fi +fi + +# ---- Step 4: Validation -------------------------------------------------- + +header "Validation" + +ERRORS=0 + +check_file() { + local label="$1" path="$2" + if [ -f "$path" ]; then + info " ${label}: OK" + else + warn " ${label}: MISSING (${path})" + ERRORS=$((ERRORS + 1)) + fi +} + +check_dir() { + local label="$1" path="$2" + if [ -d "$path" ]; then + info " ${label}: OK" + else + warn " ${label}: MISSING (${path})" + ERRORS=$((ERRORS + 1)) + fi +} + +check_file ".env" "$ENV_FILE" +check_file "docker-compose.yaml" "${SCRIPT_DIR}/docker-compose.yaml" +check_file "nginx.conf" "${SCRIPT_DIR}/deploy/nginx.conf" +check_file "SSL certificate" "$CERT_FILE" +check_file "SSL key" "$KEY_FILE" +check_dir "data/" "${SCRIPT_DIR}/data" +check_dir "logs/" "${SCRIPT_DIR}/logs" +check_dir "deploy/homebrew/" "${SCRIPT_DIR}/deploy/homebrew" + +echo "" + +if [ "$ERRORS" -gt 0 ]; then + warn "Setup completed with ${ERRORS} warning(s). Review the items above." +else + info "All checks passed!" +fi + +# ---- Step 5: Next steps --------------------------------------------------- + +header "Next Steps" + +cat <<'NEXT' +1. Review your .env file and adjust values if needed. + +2. Launch the application: + docker-compose up -d + +3. Create your first user (once containers are running): + ./docker-user.sh init # uses admin from .env + ./docker-user.sh create # or specify directly + +4. Access the site at: + https://localhost + +5. Manage users later with: + ./docker-user.sh list # List all users + ./docker-user.sh check # Check a user's status + ./docker-user.sh verify # Verify an unverified user + +6. To import homebrew content, place your .orcbrew file at: + deploy/homebrew/homebrew.orcbrew + +7. To build from source instead of pulling images: + docker-compose -f docker-compose-build.yaml build + docker-compose -f docker-compose-build.yaml up -d + +For more details, see README.md. +NEXT diff --git a/docker-user.sh b/docker-user.sh new file mode 100755 index 000000000..1ca3151e2 --- /dev/null +++ b/docker-user.sh @@ -0,0 +1,278 @@ +#!/usr/bin/env bash +# +# OrcPub Docker User Management +# +# Injects and verifies users in the Datomic database running inside Docker. +# Works by executing Clojure code inside the orcpub container, using the +# uberjar classpath (which already has datomic.api and buddy.hashers). +# +# Usage: +# ./docker-user.sh create +# ./docker-user.sh verify +# ./docker-user.sh check +# ./docker-user.sh list +# +# The script auto-detects the orcpub container name from docker-compose. +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MANAGE_SCRIPT="${SCRIPT_DIR}/docker/scripts/manage-user.clj" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +color_green='\033[0;32m' +color_red='\033[0;31m' +color_yellow='\033[1;33m' +color_reset='\033[0m' + +info() { printf '%s[OK]%s %s\n' "$color_green" "$color_reset" "$*"; } +error() { printf '%s[ERROR]%s %s\n' "$color_red" "$color_reset" "$*" >&2; } +warn() { printf '%s[WARN]%s %s\n' "$color_yellow" "$color_reset" "$*"; } + +usage() { + cat <<'USAGE' +OrcPub Docker User Management + +Usage: + ./docker-user.sh init + Create the initial admin user from INIT_ADMIN_* variables in .env + Safe to run multiple times — duplicates are skipped. + + ./docker-user.sh create + Create a new user (auto-verified, skips email) + + ./docker-user.sh batch + Create multiple users from a file (one JVM startup). + File format: one user per line — username email password + Lines starting with # and blank lines are skipped. + Duplicates are logged and skipped (not treated as errors). + + ./docker-user.sh verify + Verify an existing unverified user + + ./docker-user.sh check + Check if a user exists and show their status + + ./docker-user.sh list + List all users in the database + +Options: + --container Override container name detection + --help Show this help + +Examples: + ./docker-user.sh init + ./docker-user.sh create admin admin@example.com MySecurePass123 + ./docker-user.sh batch users.txt + ./docker-user.sh check admin + ./docker-user.sh list +USAGE +} + +# --------------------------------------------------------------------------- +# Find the orcpub container +# --------------------------------------------------------------------------- + +find_container() { + local container="" + + # Try docker-compose/docker compose service name first + if command -v docker-compose &>/dev/null; then + container=$(docker-compose ps -q orcpub 2>/dev/null || true) + fi + if [ -z "$container" ] && docker compose version &>/dev/null 2>&1; then + container=$(docker compose ps -q orcpub 2>/dev/null || true) + fi + + # Fallback: search by image name + if [ -z "$container" ]; then + container=$(docker ps -q --filter "ancestor=orcpub/orcpub:latest" 2>/dev/null | head -1 || true) + fi + + # Fallback: search by container name pattern + if [ -z "$container" ]; then + container=$(docker ps -q --filter "name=orcpub" 2>/dev/null | head -1 || true) + fi + + echo "$container" +} + +# --------------------------------------------------------------------------- +# Wait for container and Datomic to be ready +# --------------------------------------------------------------------------- + +wait_for_ready() { + local container="$1" + local max_wait=120 + local waited=0 + + # Check container is running + if ! docker inspect --format='{{.State.Running}}' "$container" 2>/dev/null | grep -q true; then + error "Container $container is not running." + error "Start it first: docker-compose up -d" + exit 1 + fi + + # Wait for Docker's native healthcheck (defined in docker-compose.yaml) + # to report the container as healthy. This avoids spawning a JVM per check. + local health + health=$(docker inspect --format='{{if .State.Health}}yes{{end}}' "$container" 2>/dev/null || true) + + if [ "$health" = "yes" ]; then + printf "Waiting for container health check" + while [ $waited -lt $max_wait ]; do + local status + status=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || true) + if [ "$status" = "healthy" ]; then + echo "" + info "Container is healthy" + return 0 + fi + if [ "$status" = "unhealthy" ]; then + echo "" + error "Container reported unhealthy" + exit 1 + fi + printf "." + sleep 2 + waited=$((waited + 2)) + done + echo "" + error "Timed out waiting for healthy status (${max_wait}s)." + exit 1 + fi + + # Fallback: no healthcheck defined — check HTTP readiness directly + warn "No Docker healthcheck found; polling HTTP on container port..." + printf "Waiting for app" + while [ $waited -lt $max_wait ]; do + if docker exec "$container" wget --no-verbose --tries=1 --spider \ + "http://localhost:${PORT:-8890}/" 2>/dev/null; then + echo "" + return 0 + fi + printf "." + sleep 2 + waited=$((waited + 2)) + done + + echo "" + error "Timed out waiting for app (${max_wait}s). Is the datomic container running?" + exit 1 +} + +# --------------------------------------------------------------------------- +# Run the management script inside the container +# --------------------------------------------------------------------------- + +run_in_container() { + local container="$1" + shift + + # Copy the management script into the container + docker cp "$MANAGE_SCRIPT" "${container}:/tmp/manage-user.clj" + + # Run it with the uberjar classpath + docker exec "$container" \ + java -cp /orcpub.jar clojure.main /tmp/manage-user.clj "$@" +} + +# --------------------------------------------------------------------------- +# Parse args and dispatch +# --------------------------------------------------------------------------- + +CONTAINER_OVERRIDE="" + +# Extract --container flag if present +ARGS=() +while [[ $# -gt 0 ]]; do + case "$1" in + --container) + CONTAINER_OVERRIDE="$2" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + ARGS+=("$1") + shift + ;; + esac +done + +set -- "${ARGS[@]+"${ARGS[@]}"}" + +if [ $# -eq 0 ]; then + usage + exit 1 +fi + +# Verify manage-user.clj exists +if [ ! -f "$MANAGE_SCRIPT" ]; then + error "Management script not found at: $MANAGE_SCRIPT" + exit 1 +fi + +# Find or use specified container +if [ -n "$CONTAINER_OVERRIDE" ]; then + CONTAINER="$CONTAINER_OVERRIDE" +else + CONTAINER=$(find_container) +fi + +if [ -z "$CONTAINER" ]; then + error "Cannot find the orcpub container." + error "Make sure the containers are running: docker-compose up -d" + exit 1 +fi + +# Wait for Datomic to be reachable, then run the command +wait_for_ready "$CONTAINER" + +# For init: read INIT_ADMIN_* from .env and create the user +if [ "${1:-}" = "init" ]; then + ENV_FILE="${SCRIPT_DIR}/.env" + if [ ! -f "$ENV_FILE" ]; then + error "No .env file found. Run ./docker-setup.sh first." + exit 1 + fi + + # Source .env to get INIT_ADMIN_* variables + # shellcheck disable=SC1090 + . "$ENV_FILE" + + if [ -z "${INIT_ADMIN_USER:-}" ]; then + error "INIT_ADMIN_USER is not set in .env" + error "Run ./docker-setup.sh to configure, or set it manually in .env" + exit 1 + fi + if [ -z "${INIT_ADMIN_EMAIL:-}" ] || [ -z "${INIT_ADMIN_PASSWORD:-}" ]; then + error "INIT_ADMIN_EMAIL and INIT_ADMIN_PASSWORD must also be set in .env" + exit 1 + fi + + info "Creating initial admin user: ${INIT_ADMIN_USER} <${INIT_ADMIN_EMAIL}>" + run_in_container "$CONTAINER" create "$INIT_ADMIN_USER" "$INIT_ADMIN_EMAIL" "$INIT_ADMIN_PASSWORD" + +# For batch: copy the user file into the container and rewrite the path +elif [ "${1:-}" = "batch" ]; then + USER_FILE="${2:-}" + if [ -z "$USER_FILE" ]; then + error "Usage: ./docker-user.sh batch " + exit 1 + fi + if [ ! -f "$USER_FILE" ]; then + error "File not found: $USER_FILE" + exit 1 + fi + docker cp "$USER_FILE" "${CONTAINER}:/tmp/batch-users.txt" + run_in_container "$CONTAINER" batch /tmp/batch-users.txt +else + run_in_container "$CONTAINER" "$@" +fi diff --git a/docker/datomic/Dockerfile b/docker/datomic/Dockerfile new file mode 100644 index 000000000..d3dda3182 --- /dev/null +++ b/docker/datomic/Dockerfile @@ -0,0 +1,28 @@ +FROM eclipse-temurin:8-jre-alpine + +ENV DATOMIC_VERSION 0.9.5703 + +RUN wget https://github.com/Orcpub/orcpub/raw/refs/heads/develop/lib/datomic-free-0.9.5703.tar.gz -qO /tmp/datomic.tar.gz \ + && cd /tmp \ + && tar -xvzf /tmp/datomic.tar.gz \ + && rm /tmp/datomic.tar.gz \ + && mv datomic-free-0.9.5703 /datomic + +WORKDIR /datomic + +RUN cp /datomic/config/samples/free-transactor-template.properties transactor.properties + +RUN mkdir /data +RUN sed "s/# data-dir=data/data-dir=\/data/" -i transactor.properties + +RUN mkdir /log +RUN sed "s/# log-dir=log/log-dir=\/log/" -i transactor.properties + +RUN sed "s/host=localhost/host=0.0.0.0/" -i transactor.properties +RUN sed "s/# storage-access=local/storage-access=remote/" -i transactor.properties + +ADD deploy/start.sh /datomic/ +RUN chmod +x /datomic/start.sh + +EXPOSE 4334 4335 4336 +CMD ["/datomic/start.sh"] diff --git a/docker/datomic/README.md b/docker/datomic/README.md new file mode 100644 index 000000000..86363b4d0 --- /dev/null +++ b/docker/datomic/README.md @@ -0,0 +1,7 @@ +# Datomic image +This dockerfile creates the datomic-free database on top of the bootstrap image + +# Contents +- [Bootstrap image](../bootstrap/README.md) +- unzip +- datomic diff --git a/docker/orcpub/Dockerfile b/docker/orcpub/Dockerfile new file mode 100644 index 000000000..8c0f1cfc5 --- /dev/null +++ b/docker/orcpub/Dockerfile @@ -0,0 +1,18 @@ +FROM clojure:lein as builder + +# Build cache layer +ADD ./lib/ /root/.m2/repository/ +WORKDIR /orcpub +COPY project.clj /orcpub/ +RUN lein deps + +ADD ./ /orcpub +RUN printenv &&\ + lein uberjar + +FROM eclipse-temurin:8-jre-alpine as runner + +COPY --from=builder /orcpub/target/orcpub.jar /orcpub.jar + +ENTRYPOINT ["java", "-jar"] +CMD ["/orcpub.jar"] diff --git a/docker/orcpub/README.md b/docker/orcpub/README.md new file mode 100644 index 000000000..b671e3c70 --- /dev/null +++ b/docker/orcpub/README.md @@ -0,0 +1,3 @@ +# Orcpub image +This dockerfile compiles orcpub using `lein uberjar` to create a single jar file. This jar file is then copied to a second, clean image, from which it can be run. + diff --git a/docker/scripts/manage-user.clj b/docker/scripts/manage-user.clj new file mode 100644 index 000000000..b02cac2ca --- /dev/null +++ b/docker/scripts/manage-user.clj @@ -0,0 +1,212 @@ +;; OrcPub Docker User Management Script +;; +;; Runs inside the orcpub container using the uberjar classpath: +;; java -cp /orcpub.jar clojure.main /scripts/manage-user.clj [args...] +;; +;; Commands: +;; create — Create and auto-verify a user +;; batch — Create users from a file (one per line) +;; verify — Verify an existing unverified user +;; check — Check if a user exists and their status +;; list — List all users (username + email + verified) + +(ns manage-user + (:require [datomic.api :as d] + [buddy.hashers :as hashers] + [clojure.string :as s])) + +(def datomic-url + (or (System/getenv "DATOMIC_URL") + "datomic:free://datomic:4334/orcpub?password=datomic")) + +(defn get-conn [] + (try + (d/connect datomic-url) + (catch Exception e + (binding [*out* *err*] + (println "ERROR: Cannot connect to Datomic at" datomic-url) + (println " Is the transactor running? Cause:" (.getMessage e))) + (System/exit 1)))) + +(defn find-user [db username-or-email] + (d/q '[:find (pull ?e [:orcpub.user/username + :orcpub.user/email + :orcpub.user/verified? + :orcpub.user/created + :db/id]) . + :in $ ?needle + :where + (or [?e :orcpub.user/username ?needle] + [?e :orcpub.user/email ?needle])] + db + username-or-email)) + +(defn try-create-user! + "Creates a user. Returns {:ok true} on success, {:duplicate \"reason\"} if the + user/email already exists, or {:error \"message\"} on unexpected failure." + [conn username email password] + (let [db (d/db conn) + email (s/lower-case (s/trim email)) + username (s/trim username)] + (cond + (d/q '[:find ?e . :in $ ?email + :where [?e :orcpub.user/email ?email]] db email) + {:duplicate (str "Email already registered: " email)} + + (d/q '[:find ?e . :in $ ?username + :where [?e :orcpub.user/username ?username]] db username) + {:duplicate (str "Username already taken: " username)} + + :else + (do + @(d/transact conn + [{:orcpub.user/email email + :orcpub.user/username username + :orcpub.user/password (hashers/encrypt password) + :orcpub.user/verified? true + :orcpub.user/send-updates? false + :orcpub.user/created (java.util.Date.)}]) + (println "OK: User created and verified —" username "<" email ">") + {:ok true})))) + +(defn batch-create-users! + "Reads a user file (one user per line: username email password) and creates + all users in a single JVM session. Blank lines and #-comments are skipped. + Duplicates are logged and skipped (not counted as failures). + Returns exit code 0 if no hard failures, 1 otherwise." + [conn path] + (let [lines (->> (s/split-lines (slurp path)) + (map s/trim) + (remove #(or (s/blank? %) (s/starts-with? % "#")))) + results (doall + (for [line lines] + (let [parts (s/split line #"\s+")] + (if (< (count parts) 3) + (do (binding [*out* *err*] + (println "SKIP: bad line (need: username email password):" line)) + {:error "bad line"}) + (let [[username email password] parts + result (try + (try-create-user! conn username email password) + (catch Exception e + {:error (.getMessage e)}))] + (when (:duplicate result) + (println "SKIP:" username "—" (:duplicate result))) + (when (:error result) + (binding [*out* *err*] + (println "FAIL:" username "—" (:error result)))) + result))))) + total (count results) + created (count (filter :ok results)) + dupes (count (filter :duplicate results)) + failed (count (filter :error results))] + (println) + (println (format "Batch complete: %d created, %d skipped (duplicate), %d failed, %d total" + created dupes failed total)) + (if (pos? failed) 1 0))) + +(defn verify-user! [conn username-or-email] + (let [db (d/db conn) + user (find-user db username-or-email)] + (if-not user + (do (binding [*out* *err*] + (println "ERROR: User not found:" username-or-email)) + (System/exit 1)) + (if (:orcpub.user/verified? user) + (println "OK: User already verified —" (:orcpub.user/username user)) + (do + @(d/transact conn + [[:db/add (:db/id user) :orcpub.user/verified? true]]) + (println "OK: User verified —" (:orcpub.user/username user))))))) + +(defn check-user [db username-or-email] + (if-let [user (find-user db username-or-email)] + (do + (println "Found user:") + (println " Username:" (:orcpub.user/username user)) + (println " Email: " (:orcpub.user/email user)) + (println " Verified:" (:orcpub.user/verified? user)) + (println " Created: " (:orcpub.user/created user))) + (do + (println "User not found:" username-or-email) + (System/exit 1)))) + +(defn list-users [db] + (let [users (d/q '[:find [(pull ?e [:orcpub.user/username + :orcpub.user/email + :orcpub.user/verified?]) ...] + :where [?e :orcpub.user/username]] + db)] + (if (empty? users) + (println "No users found.") + (do + (println (format "%-20s %-30s %s" "USERNAME" "EMAIL" "VERIFIED")) + (println (apply str (repeat 65 "-"))) + (doseq [u (sort-by :orcpub.user/username users)] + (println (format "%-20s %-30s %s" + (:orcpub.user/username u) + (:orcpub.user/email u) + (:orcpub.user/verified? u)))))))) + +;; --- CLI dispatch --- + +(let [args *command-line-args* + cmd (first args)] + (case cmd + "create" (let [[_ username email password] args] + (when-not (and username email password) + (binding [*out* *err*] + (println "Usage: manage-user.clj create ")) + (System/exit 1)) + (let [conn (get-conn) + result (try-create-user! conn username email password)] + (when-let [msg (or (:duplicate result) (:error result))] + (binding [*out* *err*] + (println "ERROR:" msg)) + (System/exit 1)))) + + "batch" (let [[_ path] args] + (when-not path + (binding [*out* *err*] + (println "Usage: manage-user.clj batch ") + (println " File format: one user per line — username email password") + (println " Lines starting with # and blank lines are skipped")) + (System/exit 1)) + (let [conn (get-conn) + exit (batch-create-users! conn path)] + (System/exit exit))) + + "verify" (let [[_ username-or-email] args] + (when-not username-or-email + (binding [*out* *err*] + (println "Usage: manage-user.clj verify ")) + (System/exit 1)) + (let [conn (get-conn)] + (verify-user! conn username-or-email))) + + "check" (let [[_ username-or-email] args] + (when-not username-or-email + (binding [*out* *err*] + (println "Usage: manage-user.clj check ")) + (System/exit 1)) + (let [conn (get-conn) + db (d/db conn)] + (check-user db username-or-email))) + + "list" (let [conn (get-conn) + db (d/db conn)] + (list-users db)) + + (do + (println "OrcPub User Management") + (println "") + (println "Commands:") + (println " create Create and auto-verify a user") + (println " batch Create users from a file (one per line)") + (println " verify Verify an existing user") + (println " check Check if a user exists") + (println " list List all users") + (when-not cmd + (System/exit 1)))) + ;; Datomic peer threads are non-daemon and keep the JVM alive; force exit. + (System/exit 0)) diff --git a/docker/scripts/users.example.txt b/docker/scripts/users.example.txt new file mode 100644 index 000000000..2d0404dc4 --- /dev/null +++ b/docker/scripts/users.example.txt @@ -0,0 +1,13 @@ +# Batch user file template +# +# Copy this file, replace the example users with your own, then run: +# cp docker/scripts/users.example.txt users.txt +# ./docker-user.sh batch users.txt +# +# Format: username email password (whitespace-separated) +# Lines starting with # and blank lines are skipped. +# Duplicates are logged and skipped (not treated as errors). + +admin admin@example.com SecurePass123 +player1 player1@example.com AnotherPass456 +player2 player2@example.com YetAnotherPass789 diff --git a/docs/CONFLICT_RESOLUTION.md b/docs/CONFLICT_RESOLUTION.md new file mode 100644 index 000000000..fb66755b0 --- /dev/null +++ b/docs/CONFLICT_RESOLUTION.md @@ -0,0 +1,222 @@ +# Conflict Resolution & Duplicate Key Handling + +## Overview + +Detects and resolves duplicate content keys during import, preventing data loss when merging homebrew content from multiple sources. + +**Why this exists:** Before this feature, imports silently overwrote existing content with no warning. Characters referencing the old version would break. Users needed explicit control over conflict resolution. + +**Key insight:** When renaming parent content (e.g., a class), all child references (subclasses) must be updated automatically or they become orphaned. This was the critical bug that led to the reference update feature. + +## How It Works + +### Duplicate Key Detection + +Scans imported content for duplicate keys before import. + +**Two conflict types:** + +**External** - Between existing and imported content: +``` +Already loaded: :artificer from "Player's Handbook" +Importing: :artificer from "Homebrew Classes" +→ CONFLICT: Same key, different sources +``` + +**Internal** - Within the imported file: +``` +Importing from "My Pack": + - :artificer (Battle Smith subclass) + - :artificer (Armorer subclass) +→ CONFLICT: Same key used twice +``` + +**Implementation:** `import_validation.cljs:162-280` + +### Conflict Resolution Modal + +Interactive modal presents three resolution options per conflict. + +**1. Rename** - Give new key to imported item +``` +Option: Rename imported :artificer to :artificer-2 +Result: Both versions exist with different keys +Use when: You want to keep both versions +``` + +**2. Skip** - Don't import this item +``` +Option: Skip :artificer from "Homebrew Classes" +Result: Existing version unchanged, new version discarded +Use when: You prefer the existing version +``` + +**3. Replace** - Overwrite existing with imported +``` +Option: Replace :artificer with new version +Result: Existing version removed, new version loaded +Use when: You want to update to the new version +``` + +**Bulk actions:** Rename All, Skip All, Replace All (for handling 10+ conflicts efficiently) + +**Design decision:** Originally considered automatic resolution strategies (always rename, always skip). User testing showed explicit per-conflict choices prevent unexpected behavior and data loss. + +**Implementation:** `views/conflict_resolution.cljs` + +### Key Renaming with Reference Updates + +When renaming, automatically updates all internal references to maintain parent-child relationships. + +```clojure +;; Original +{:key :artificer + :name "Artificer"} + +{:key :battle-smith + :class :artificer ; ← Reference to parent + :name "Battle Smith"} + +;; After renaming :artificer → :artificer-2 +{:key :artificer-2 ; ← Renamed + :name "Artificer"} + +{:key :battle-smith + :class :artificer-2 ; ← Auto-updated! + :name "Battle Smith"} +``` + +**Why this matters:** Early implementation forgot to update references. Result: Subclasses imported successfully but became orphaned (not associated with parent class). Character builder showed them but they were unselectable. + +**Reference types supported:** +- Subclass → parent class (`:class` field) +- Subrace → parent race (`:race` field) +- Items → class restrictions (`:classes` field) +- Spells → class spell lists (`:spell-lists` field) + +**Implementation:** `import_validation.cljs:282-380` + +### Auto-generated Keys + +"Rename All" appends numeric suffix until key is unique. + +```clojure +:artificer → :artificer-2 +:artificer-2 → :artificer-3 +:blood-hunter → :blood-hunter-2 +``` + +Checks against all existing content (loaded + importing) to guarantee uniqueness. + +**Alternative considered:** UUIDs (`:artificer-a3f2b1c9`). Rejected: not human-readable, harder to debug. + +**Implementation:** `events.cljs:450-520` + +## Common Scenarios + +### Single Conflict + +Importing `:artificer` when PHB `:artificer` already exists: + +``` +┌─ Conflict Resolution ─────────────────────────┐ +│ Found 1 duplicate key: │ +│ │ +│ Classes: │ +│ :artificer │ +│ Existing: "Player's Handbook" │ +│ Importing: "Homebrew Classes" │ +│ │ +│ ○ Rename to: :artificer-2 │ +│ ○ Skip (keep existing) │ +│ ○ Replace (use imported) │ +│ │ +│ [Cancel] [Apply Resolutions] │ +└────────────────────────────────────────────────┘ +``` + +Choosing "Rename" → Both versions available (`:artificer` and `:artificer-2`) + +### Multiple Conflicts + +Importing 5 classes where 3 conflict with existing content. Click "Rename All" → all auto-renamed (`:artificer-2`, `:blood-hunter-2`, `:mystic-2`). Faster than resolving individually. + +### Subclass Reference Updates (Critical) + +Importing custom fighter with subclasses, conflicts with existing `:custom-fighter`: + +``` +Before rename: + Class: :custom-fighter + Subclass: :rune-knight → parent: :custom-fighter + +After renaming to :custom-fighter-2: + Class: :custom-fighter-2 + Subclass: :rune-knight → parent: :custom-fighter-2 ← Auto-updated +``` + +Without auto-update, subclass becomes orphaned (shows in UI but unselectable). + +## Implementation + +**How the modal appears (integration flow):** + +1. **Import triggered** - User imports via `::e5/import-plugin` event (`events.cljs:3314`) +2. **Validation runs** - Checks for duplicate keys (`events.cljs:3318-3329`) +3. **Conflicts found?** - If yes, dispatch `:start-conflict-resolution` (`events.cljs:3353-3360`) +4. **State updated** - Event sets `:conflict-resolution {:active? true ...}` (`events.cljs:3466-3475`) +5. **Modal subscribes** - Component subscribes to `:conflict-resolution` (`subs.cljs:1296`, `views/conflict_resolution.cljs`) +6. **Conditional render** - `(when active? ...)` shows modal (`views/conflict_resolution.cljs`) +7. **Always mounted** - Modal part of `import-log-overlay` rendered in `main-view` (`core.cljs:113`) + +**Key files:** +- `import_validation.cljs` - Conflict detection logic +- `events.cljs:3314-3575` - Import event, conflict check, resolution events +- `views/conflict_resolution.cljs` - Modal component + overlay container +- `core.cljs:106-113` - App root (mounts overlay on every page) +- `subs.cljs:1296-1313` - State subscriptions +- `import_validation_test.cljs` - Tests + +**Reference fields map** (for adding new content types): +```clojure +{:subclass [:class] ; Parent class + :subrace [:race] ; Parent race + :spell [:spell-lists] ; Which classes can cast + :item [:classes]} ; Class restrictions +``` + +## Testing + +**Automated:** `import_validation_test.cljs` - Covers duplicate detection, key renaming, reference updates, unique key generation + +**Critical manual test:** Create class + subclass → Export → Delete → Re-import with rename → Verify subclass still references renamed parent + +**Why this test matters:** This is the bug that led to the reference update feature. Must not regress. + +## Extending + +**Change naming pattern:** Edit `generate-unique-key` in `events.cljs` (currently appends `-2`, `-3`, etc.) + +**Add reference types:** Add to `reference-fields` map in `import_validation.cljs` (shown in Implementation section above) + +## Troubleshooting + +**Modal doesn't appear:** Check console for conflict detection logs. Verify import actually contains duplicate keys. + +**References not updated:** Reference field probably not in `reference-fields` map. Add to `import_validation.cljs` and re-run. + +**Duplicate keys after "Rename All":** Bug in `generate-unique-key`. Verify `existing-keys` includes all loaded content (not just imported). + +## Future Enhancements + +**Preview impact:** Show what renaming will affect before applying (e.g., "Will update 3 subclasses, 12 spell references") + +**Conflict history:** Remember previous decisions for same content ("Last time: Rename - apply again?") + +**Diff view:** Compare conflicting versions side-by-side to make informed choice + +## Related Documentation + +- [ORCBREW_FILE_VALIDATION.md](ORCBREW_FILE_VALIDATION.md) - Import/export validation +- [CONTENT_RECONCILIATION.md](CONTENT_RECONCILIATION.md) - Missing content detection +- [HOMEBREW_REQUIRED_FIELDS.md](HOMEBREW_REQUIRED_FIELDS.md) - Content field requirements diff --git a/docs/CONTENT_RECONCILIATION.md b/docs/CONTENT_RECONCILIATION.md new file mode 100644 index 000000000..e9e9de6f2 --- /dev/null +++ b/docs/CONTENT_RECONCILIATION.md @@ -0,0 +1,128 @@ +# Content Reconciliation & Missing Content Detection + +## Overview + +Detects when characters reference missing homebrew content and suggests alternatives using fuzzy matching. + +**Why this exists:** User deletes homebrew plugin, reopens character, sees `:artificer (not loaded)` with no context. No way to know which plugin to reinstall or what similar content exists. + +**Design decision:** Use multiple fuzzy matching strategies (exact key, Levenshtein distance, prefix matching) to catch common cases: typos, versioning (`:blood-hunter-v2`), and renamed content. + +**Key gotcha:** Must exclude built-in content (PHB, Xanathar's, etc.) or system suggests switching from homebrew Artificer to PHB Artificer (which doesn't exist in most books). + +## How It Works + +### Missing Content Detection + +Scans character options tree for `::entity/key` references → Checks if key exists in loaded content → Reports missing with suggestions + +**Supported types:** Classes, subclasses, races, subraces, backgrounds + +**Implementation:** `content_reconciliation.cljs` + +### Fuzzy Matching + +Four strategies find similar content: + +**1. Exact key, different source** +``` +Missing: :artificer from "Serakat's Compendium" +Suggests: :artificer from "Player's Handbook" +``` + +**2. Levenshtein distance** (max 3 edits for typos) +``` +Missing: :artficer +Suggests: :artificer +``` + +**3. Prefix matching** (min 4 chars, for versioning) +``` +Missing: :battle-smith-v2 +Suggests: :battle-smith +``` + +**4. Display name similarity** (max 3 edits) +``` +Missing: :drunken_master +Suggests: :drunken-master +``` + +**Why multiple strategies:** Single strategy missed too many cases. Levenshtein alone doesn't catch versioning (`:fighter-v2`). Prefix alone doesn't catch typos (`:artficer`). Combined approach catches ~80% of common cases. + +### Warning UI + +Displays in character builder: +``` +:missing-content (not loaded) +:missing-content (not loaded - try :suggested-content?) +:missing-content from "Plugin Name" (not loaded - try :suggested-content?) +``` + +**Implementation:** `views.cljs` (display), `subs.cljs` (subscriptions) + +### Built-in Content Exclusions + +Excludes PHB, Xanathar's, Tasha's, and 9 other official books from warnings. + +**Why:** Built-in content is always available. Without exclusion, system suggests "try PHB Artificer" when user's homebrew Artificer is missing (but PHB doesn't have Artificer in 5e). + +## Common Scenarios + +**Deleted plugin:** Character shows `:rune-knight from "Fighter Subclasses" (not loaded - try :eldritch-knight?)` → Re-import plugin or use suggested alternative + +**Shared character:** Friend's character uses homebrew → Warnings show which plugins needed → Ask friend for files or use suggested official alternatives + +**Renamed content:** Updated `:blood-hunter` to `:blood-hunter-v2` → Old characters suggest new version → Prefix matching catches versioning + +## Implementation + +**Key files:** +- `content_reconciliation.cljs` - Detection, fuzzy matching (`find-missing-content`, `find-suggestion`, `levenshtein-distance`) +- `subs.cljs`, `views.cljs` - UI integration (subscriptions, warning display) +- `common.cljc` - Utilities (`kw-base`, `traverse-nested`) +- `import_validation_test.cljs` - Tests + +**Data flow:** +Character loaded → `extract-character-keys` → `classify-content-type` → `find-available-content` → Missing? → `find-suggestion` → Display warning with suggestion + +**Performance:** ~10ms detection + ~5ms per missing item for fuzzy matching. 100+ missing items may need optimization. + +## Testing + +**Automated:** `import_validation_test.cljs` - Covers detection, fuzzy matching accuracy, built-in exclusions + +**Critical manual tests:** +1. Delete plugin → Reopen character → Should show "(not loaded)" warning +2. Rename `:blood-hunter` to `:blood-hunter-v2` → Should suggest new version +3. PHB Wizard with Evocation → Should NOT warn (built-in exclusion) + +## Extending + +**Adjust thresholds:** Edit `levenshtein-distance-threshold`, `prefix-match-length`, `name-similarity-threshold` in `content_reconciliation.cljs` (defaults: 3, 4, 3) + +**Add content types:** Add to `content-type-paths` and `content-type->field` maps + +**Exclude sources:** Add to `built-in-sources` set + +## Troubleshooting + +**"Not loaded" but exists:** Check key matches exactly (`:blood-hunter` vs `:bloodhunter`), verify plugin loaded + +**Wrong suggestions:** Adjust matching thresholds, check for duplicate keys + +**Built-in showing warnings:** Source name doesn't match exclusion list exactly, add variant to `built-in-sources` + +## Future Enhancements + +**Auto-fix button:** One-click apply suggestion + +**Smart migration:** Auto-update characters when content renamed (detect renames, prompt to update all affected characters) + +**Plugin recommendations:** Suggest which plugin to install based on missing content library lookup + +## Related Documentation + +- [ORCBREW_FILE_VALIDATION.md](ORCBREW_FILE_VALIDATION.md) - Import/export validation +- [CONFLICT_RESOLUTION.md](CONFLICT_RESOLUTION.md) - Duplicate key handling +- [HOMEBREW_REQUIRED_FIELDS.md](HOMEBREW_REQUIRED_FIELDS.md) - Content field requirements diff --git a/docs/ERROR_HANDLING.md b/docs/ERROR_HANDLING.md new file mode 100644 index 000000000..afed80ba4 --- /dev/null +++ b/docs/ERROR_HANDLING.md @@ -0,0 +1,238 @@ +# Error Handling Guide + +This document describes the error handling approach used throughout the OrcPub application. + +## Overview + +The application uses a consistent, DRY approach to error handling built on Clojure's `ex-info` for structured exceptions. All error handling utilities are centralized in the `orcpub.errors` namespace. + +## Core Principles + +1. **User-Friendly Messages**: All errors include clear, actionable messages for end users +2. **Structured Data**: Errors use `ex-info` with structured data for programmatic handling +3. **Logging**: All errors are logged with context for debugging +4. **Fail Fast**: Operations fail immediately with clear errors rather than silently continuing +5. **DRY**: Common error handling patterns use reusable macros and utilities + +## Error Handling Utilities + +### Database Operations + +Use `with-db-error-handling` for all database transactions: + +```clojure +(require '[orcpub.errors :as errors]) + +(defn save-user [conn user-data] + (errors/with-db-error-handling :user-save-failed + {:username (:username user-data)} + "Unable to save user. Please try again." + @(d/transact conn [user-data]))) +``` + +**Benefits:** +- Automatically logs database errors with context +- Creates user-friendly error messages +- Includes structured error codes for programmatic handling +- Re-throws `ExceptionInfo` as-is (for already-handled errors) + +### Email Operations + +Use `with-email-error-handling` for sending emails: + +```clojure +(defn send-welcome-email [user-email] + (errors/with-email-error-handling :welcome-email-failed + {:email user-email} + "Unable to send welcome email. Please contact support." + (postal/send-message config message))) +``` + +**Benefits:** +- Handles SMTP connection failures gracefully +- Logs email failures for ops monitoring +- Prevents application crashes when email server is down + +### Validation & Parsing + +Use `with-validation` for parsing user input: + +```clojure +(defn parse-user-id [id-string] + (errors/with-validation :invalid-user-id + {:id-string id-string} + "Invalid user ID format. Expected a number." + (Long/parseLong id-string))) +``` + +**Benefits:** +- Handles `NumberFormatException` and other parsing errors +- Provides clear validation error messages +- Includes the invalid input in error data for debugging + +## Error Data Structure + +All errors created by the utilities include: + +```clojure +{:error :error-code-keyword ; Machine-readable error type + ;; Additional context fields specific to the operation + :user-id 123 + :operation-specific-data "..."} +``` + +Example exception: + +```clojure +(ex-info "Unable to save user. Please try again." + {:error :user-save-failed + :username "alice" + :message "Connection timeout"} + original-exception) +``` + +## Error Codes + +Error codes are defined as keywords in `orcpub.errors`: + +### Database Errors +- `:party-creation-failed` - Failed to create a party +- `:party-update-failed` - Failed to update party data +- `:party-remove-character-failed` - Failed to remove character from party +- `:party-deletion-failed` - Failed to delete party +- `:verification-failed` - Failed to create verification record +- `:password-reset-failed` - Failed to initiate password reset +- `:password-update-failed` - Failed to update password +- `:entity-creation-failed` - Failed to create entity +- `:entity-update-failed` - Failed to update entity + +### Email Errors +- `:verification-email-failed` - Failed to send verification email +- `:reset-email-failed` - Failed to send password reset email +- `:invalid-port` - Invalid email server port configuration + +### Validation Errors +- `:invalid-character-id` - Invalid character ID format +- `:invalid-pdf-data` - Invalid PDF request data + +### PDF Errors +- `:image-load-timeout` - Image loading timed out +- `:unknown-host` - Unknown host for image URL +- `:invalid-image-format` - Invalid or corrupt image +- `:image-load-failed` - Generic image loading failure +- `:jpeg-load-failed` - JPEG-specific loading failure + +## Testing Error Handling + +All error handling utilities are fully tested. See `test/clj/orcpub/errors_test.clj` for examples: + +```clojure +(deftest test-with-db-error-handling + (testing "wraps exceptions with proper context" + (try + (errors/with-db-error-handling :db-test-error + {:user-id 789} + "Unable to save to database" + (throw (Exception. "Connection timeout"))) + (is false "Should have thrown exception") + (catch clojure.lang.ExceptionInfo e + (is (= "Unable to save to database" (.getMessage e))) + (is (= :db-test-error (:error (ex-data e)))) + (is (= 789 (:user-id (ex-data e)))))))) +``` + +## Migration Guide + +### Before (Bespoke Error Handling) + +```clojure +(defn create-party [conn party] + (try + (let [result @(d/transact conn [party])] + {:status 200 :body result}) + (catch Exception e + (println "ERROR: Failed to create party:" (.getMessage e)) + (throw (ex-info "Unable to create party. Please try again." + {:error :party-creation-failed} + e))))) +``` + +### After (DRY with Utilities) + +```clojure +(defn create-party [conn party] + (errors/with-db-error-handling :party-creation-failed + {:party-data party} + "Unable to create party. Please try again." + (let [result @(d/transact conn [party])] + {:status 200 :body result}))) +``` + +**Benefits of migration:** +- 7 lines → 5 lines (30% reduction) +- Consistent error logging format +- No need to remember logging syntax +- Easier to read and maintain + +## Best Practices + +### DO: +- Use the provided macros for common operations +- Include relevant context in error data +- Write user-friendly error messages +- Test error handling paths + +### DON'T: +- Catch exceptions without re-throwing +- Use generic error messages like "An error occurred" +- Log errors without context +- Silently swallow exceptions + +## Client-Side API Response Handling + +All API-calling re-frame subscriptions use the `handle-api-response` HOF from `events.cljs`: + +```clojure +(require '[orcpub.dnd.e5.events :refer [handle-api-response]]) + +;; Basic usage — 401 routes to login, 500 shows generic error +(handle-api-response response + #(dispatch [::set-data (:body response)]) + :context "fetch characters") + +;; Custom overrides +(handle-api-response response + #(dispatch [::set-data (:body response)]) + :on-401 #(when-not login-optional? (dispatch [:route-to-login])) + :on-500 #(when required? (dispatch (show-generic-error))) + :context "fetch user") +``` + +**Defaults:** +- 200: calls `on-success` +- 401: dispatches `:route-to-login` (override with `:on-401`) +- 500: dispatches `show-generic-error` (override with `:on-500`) +- Any other status: logs to console with `:context` string + +This prevents the class of bug where a bare `case` with no default clause crashes on unexpected HTTP statuses. + +## Future Improvements + +Potential enhancements to consider: + +1. **Retry Logic**: Add automatic retry for transient failures (network, db) +2. **Circuit Breakers**: Prevent cascading failures in external dependencies +3. **Error Monitoring**: Integration with error tracking services (Sentry, Rollbar) +4. **Rate Limiting**: Add rate limiting context to prevent abuse +5. **Internationalization**: Support multiple languages for error messages + +## Related Files + +- `src/cljc/orcpub/errors.cljc` - Error handling utilities (backend) +- `src/cljs/orcpub/dnd/e5/events.cljs` - `handle-api-response` HOF (client-side) +- `test/clj/orcpub/errors_test.clj` - Comprehensive test suite +- `src/clj/orcpub/email.clj` - Email operations with error handling +- `src/clj/orcpub/datomic.clj` - Database connection with error handling +- `src/clj/orcpub/routes.clj` - HTTP routes with error handling +- `src/clj/orcpub/routes/party.clj` - Party operations (demonstrates DRY refactoring) +- `src/clj/orcpub/pdf.clj` - PDF generation with timeout and error handling diff --git a/docs/HOMEBREW_REQUIRED_FIELDS.md b/docs/HOMEBREW_REQUIRED_FIELDS.md new file mode 100644 index 000000000..0e2299d67 --- /dev/null +++ b/docs/HOMEBREW_REQUIRED_FIELDS.md @@ -0,0 +1,201 @@ +# Homebrew Required Fields + +This document tracks which fields are required for each homebrew content type. +Fields marked "SPEC REQUIRED" are validated by clojure.spec. +Fields marked "FUNCTIONAL REQUIRED" will break features if missing (PDF export, character building, etc.) + +## Legend +- **SPEC**: Defined in spec as `:req-un` +- **FUNCTIONAL**: Will break something if missing/empty +- **DEFAULT**: Can have a sensible default applied +- **OPTIONAL**: Truly optional, no issues if missing + +--- + +## Classes (::homebrew-class) + +**Spec file**: `src/cljc/orcpub/dnd/e5/classes.cljc` + +| Field | SPEC | FUNCTIONAL | DEFAULT | Notes | +|-------|------|------------|---------|-------| +| `:name` | YES | YES | - | Display name | +| `:key` | YES | YES | - | Unique identifier | +| `:option-pack` | YES | YES | "Unnamed Content" | Source/library name | +| `:hit-die` | NO | **YES** | 6 | `options.cljc:2630` - string interpolation without nil-guard | +| `:ability-increase-levels` | NO | **YES** | [4,8,12,16,19] | `options.cljc:2742` - passed to `set()` without nil-guard | +| `:level-modifiers` | NO | CONDITIONAL | [] | Breaks only if `:traits` also nil | +| `:traits` | NO | **YES** | [] | `options.cljc:2782` - passed to `filter()` without nil-guard | +| `:spellcasting` | NO | NO | - | Uses `some->` with graceful nil-handling | + +**Breaking code locations:** +- `options.cljc:2630`: `{::t/name (str "Roll (1D" die ")")}` → produces "Roll (1Dnil)" +- `options.cljc:2635`: `(dice/die-mean-round-up die)` → dies if nil +- `options.cljc:2742`: `(set ability-increase-levels)` → fails if nil +- `options.cljc:2782`: `(filter ... traits)` → can't iterate nil + +--- + +## Subclasses (::homebrew-subclass) + +**Spec file**: `src/cljc/orcpub/dnd/e5/classes.cljc` + +| Field | SPEC | FUNCTIONAL | DEFAULT | Notes | +|-------|------|------------|---------|-------| +| `:name` | YES | YES | - | Display name | +| `:key` | YES | YES | - | Unique identifier | +| `:class` | YES | YES | - | Parent class key | +| `:option-pack` | YES | YES | "Unnamed Content" | Source/library name | +| `:level-modifiers` | NO | NO | [] | Handled gracefully with `some->` | + +--- + +## Races (::homebrew-race) + +**Spec file**: `src/cljc/orcpub/dnd/e5/races.cljc` + +| Field | SPEC | FUNCTIONAL | DEFAULT | Notes | +|-------|------|------------|---------|-------| +| `:name` | YES | YES | - | Display name | +| `:key` | YES | YES | - | Unique identifier | +| `:option-pack` | YES | YES | "Unnamed Content" | Source/library name | +| `:languages` | OPT | NO | - | Optional in spec, not in critical paths | +| `:speed` | NO | **YES** | 30 | `options.cljc:1984` - compared without nil-guard in subrace | +| `:abilities` | NO | NO | {} | Uses `:or` defaults | +| `:size` | NO | NO | "Medium" | Uses `some->` or defaults | +| `:darkvision` | NO | NO | - | Uses conditional checks | + +**Breaking code location:** +- `options.cljc:1984`: `(not= speed (:speed race))` - accessed without nil-checking + +--- + +## Subraces (::homebrew-subrace) + +**Spec file**: `src/cljc/orcpub/dnd/e5/races.cljc` + +| Field | SPEC | FUNCTIONAL | DEFAULT | Notes | +|-------|------|------------|---------|-------| +| `:name` | YES | YES | - | Display name | +| `:key` | YES | YES | - | Unique identifier | +| `:race` | YES | YES | - | Parent race key | +| `:option-pack` | YES | YES | "Unnamed Content" | Source/library name | + +--- + +## Backgrounds (::homebrew-background) + +**Spec file**: `src/cljc/orcpub/dnd/e5/backgrounds.cljc` + +| Field | SPEC | FUNCTIONAL | DEFAULT | Notes | +|-------|------|------------|---------|-------| +| `:name` | YES | YES | - | Display name | +| `:key` | YES | YES | - | Unique identifier | +| `:option-pack` | YES | YES | "Unnamed Content" | Source/library name | + +--- + +## Feats (::homebrew-feat) + +**Spec file**: `src/cljc/orcpub/dnd/e5/feats.cljc` + +| Field | SPEC | FUNCTIONAL | DEFAULT | Notes | +|-------|------|------------|---------|-------| +| `:name` | YES | YES | - | Display name | +| `:key` | YES | YES | - | Unique identifier | +| `:option-pack` | YES | YES | "Unnamed Content" | Source/library name | + +--- + +## Spells (::homebrew-spell) + +**Spec file**: `src/cljc/orcpub/dnd/e5/spells.cljc` + +| Field | SPEC | FUNCTIONAL | DEFAULT | Notes | +|-------|------|------------|---------|-------| +| `:name` | YES | YES | - | Display name | +| `:key` | YES | YES | - | Unique identifier | +| `:option-pack` | YES | YES | "Unnamed Content" | Source/library name | +| `:level` | YES | YES | - | `spells.cljc:26` - required by spec | +| `:school` | YES | YES | - | `spells.cljc:26` - required by spec, used in spell card | +| `:spell-lists` | YES | YES | - | `spells.cljc:45-47` - required for homebrew | + +--- + +## Monsters (::homebrew-monster) + +**Spec file**: `src/cljc/orcpub/dnd/e5/monsters.cljc` + +| Field | SPEC | FUNCTIONAL | DEFAULT | Notes | +|-------|------|------------|---------|-------| +| `:name` | YES | YES | - | Display name | +| `:key` | YES | YES | - | Unique identifier | +| `:option-pack` | YES | YES | "Unnamed Content" | Source/library name | +| `:hit-points` | YES | YES | - | `monsters.cljc:15` - required by spec | + +--- + +## Languages (::homebrew-language) + +**Spec file**: `src/cljc/orcpub/dnd/e5/languages.cljc` + +| Field | SPEC | FUNCTIONAL | DEFAULT | Notes | +|-------|------|------------|---------|-------| +| `:name` | YES | YES | - | Display name | +| `:key` | YES | YES | - | Unique identifier | +| `:option-pack` | YES | YES | "Unnamed Content" | Source/library name | + +--- + +## Invocations (::homebrew-invocation) + +**Spec file**: `src/cljc/orcpub/dnd/e5/classes.cljc` + +| Field | SPEC | FUNCTIONAL | DEFAULT | Notes | +|-------|------|------------|---------|-------| +| `:name` | YES | YES | - | Display name | +| `:key` | YES | YES | - | Unique identifier | +| `:option-pack` | YES | YES | "Unnamed Content" | Source/library name | + +--- + +## Known Issues + +### nil nil key-value pairs +- **Symptom**: `{nil nil, :key :foo}` in exported content +- **Cause**: Empty fields serialized during export, likely from missing functional-required fields +- **Impact**: Causes PDF export black screen +- **Fix**: Cleaned on import (v0.05), should prevent at export with validation + +### Empty option-pack +- **Symptom**: `:option-pack ""` +- **Cause**: User didn't fill in source name +- **Impact**: Content appears under unnamed source +- **Fix**: Cleaned on import, defaults to "Unnamed Content" + +--- + +## Recommendations + +### Immediate: Add nil-guards +These code paths should have nil-guards added: + +1. `options.cljc:2630` - Add `(or die 6)` before string interpolation +2. `options.cljc:2742` - Add `(or ability-increase-levels [])` before set creation +3. `options.cljc:2782` - Add `(or traits [])` before filter +4. `options.cljc:1984` - Add nil-check before `(not= speed (:speed race))` + +### Export Validation +Add these to spec as `:req-un` for homebrew content: +- `::homebrew-class` needs `:hit-die` +- `::homebrew-class` needs `:ability-increase-levels` +- `::homebrew-class` needs `:traits` +- `::homebrew-race` needs `:speed` + +--- + +## Completed (2026-01-15) + +- [x] Test each "?" field to determine if it breaks functionality +- [ ] Add export validation to prevent incomplete content +- [x] Document what breaks (PDF, character builder, etc.) for each field +- [x] Identify likely source of nil nil entries (empty functional-required fields) diff --git a/docs/LANGUAGE_SELECTION_FIX.md b/docs/LANGUAGE_SELECTION_FIX.md new file mode 100644 index 000000000..9566cb4a2 --- /dev/null +++ b/docs/LANGUAGE_SELECTION_FIX.md @@ -0,0 +1,187 @@ +# Ranger Favored Enemy Language Fix + +## Overview + +Fixes GitHub issue #296: Ranger favored enemy language selection produces nil options, corrupting character data. + +**Root cause:** `language-selection` did `(map language-map keys)` on language keys from `favored-enemy-types`. Keys like `:aquan`, `:gith`, `:bullywug` aren't in the base 16 languages, so the lookup returned nil. Nil flowed through `language-option` → `modifiers/language nil` → corrupted character data. + +**Fix:** Three-layer fallback in `language-selection`: language-map lookup → corrections shim → generated entry. Never returns nil. + +## The Corruption Chain (Before Fix) + +``` +favored-enemy-types + → creature type has language keys (e.g., :fey → [:draconic :elvish ... :aquan]) + → language-selection does (map language-map keys) + → language-map has 16 base languages, :aquan is NOT one of them + → returns nil for :aquan + → language-option receives nil name/key + → modifiers/language nil + → nil language persisted to character +``` + +24 of 53 unique language keys across `favored-enemy-types` and `humanoid-enemies` are not in the base 16 languages. These are exotic/creature-specific D&D languages (Aquan, Gith, Bullywug, etc.) that homebrew plugins may define. + +## The Fix + +### Three-Layer Fallback (`options.cljc:819-828`) + +```clojure +(defn language-selection [language-map language-options] + (let [{lang-num :choose lang-options :options} language-options + languages (if (:any lang-options) + (vals language-map) + (map (fn [k] + (or (language-map k) ; 1. Exact match + (language-map (language-key-corrections k)) ; 2. Corrections shim + {:name (key-to-name k) :key k})) ; 3. Generated fallback + (keys lang-options)))] + (language-selection-aux languages lang-num))) +``` + +**Layer 1 - Language map lookup:** Checks the dynamic language-map (base 16 + any plugin-defined languages). If a homebrew plugin defines Aquan, it's used directly. + +**Layer 2 - Corrections shim:** Handles legacy/misspelled keys. Currently maps `:primoridial` → `:primordial`. Existing characters may have saved the typo; this ensures they resolve correctly. + +**Layer 3 - Generated fallback:** Creates `{:name (key-to-name k) :key k}` from the keyword itself. `:aquan` becomes `{:name "Aquan" :key :aquan}`. Guarantees a non-nil result for any key. + +### Corrections Map (`options.cljc:812-817`) + +```clojure +(def ^:private language-key-corrections + {:primoridial :primordial}) +``` + +The `:primoridial` typo existed in the fey enemy type data. Fixing the typo directly would break existing characters that saved the misspelled key. The corrections map acts as a backwards-compatible shim. + +The typo was also fixed in the source data (`:fey` now uses `:primordial`), so new characters get the correct key. Old characters with `:primoridial` still resolve via the shim. + +## Key Insight: Two Different "Key" Concepts + +This is the most important architectural detail for understanding this fix. + +### Language `:key` (data key) + +The keyword like `:aquan`, `:elvish`. Stored in `favored-enemy-types`, used by `modifiers/language` to apply the language proficiency to a character. This is what the fix operates on. + +### Option `::entity/key` (template key) + +Derived from the display name via `name-to-kw`. When `language-option` creates an option, it calls `option-cfg` which does: + +```clojure +{::key (or key (common/name-to-kw name))} +``` + +`language-option` does NOT pass `:key` to `option-cfg`, so the option `::key` is always derived from the display name. For "Aquan" → `:aquan`. This is what gets saved in character data for option matching. + +**Why this matters:** The option tree matching system (`entity-item-with-key`) matches by `::entity/key`, not by language `:key`. So `key-to-name` and `name-to-kw` must round-trip correctly for saved characters to match their options. + +### Round-Trip Safety + +`key-to-name` (`:aquan` → `"Aquan"`) and `name-to-kw` (`"Aquan"` → `:aquan`) are inverses for standard naming. The fallback entry `{:name "Aquan" :key :aquan}` produces an option with `::key :aquan`, which matches any saved character selection with `::key :aquan`. + +This breaks for non-standard naming (e.g., if a plugin defines Aquan as "Water Primordial"). In that case the option `::key` would be `:water-primordial`, not `:aquan`. But the fallback only activates when the plugin ISN'T loaded, so this is the correct behavior -- when the plugin IS loaded, layer 1 uses the plugin's definition. + +## Language Sources in the System + +### Where languages come from + +The language-map is dynamic: base 16 languages + plugin-defined languages. + +``` +spell_subs.cljs subscription chain: + ::language-map ← ::languages ← ::plugin-languages ← ::plugin-vals + + plugin-languages = (mapcat (comp vals ::e5/languages) plugins) + final = (concat base-languages plugin-languages) +``` + +Languages are ONLY added to the map from the `:orcpub.dnd.e5/languages` content type in orcbrew files. There is a dedicated Language Builder in the homebrew UI for creating them. + +### Where languages do NOT come from + +- **Monster definitions:** Monster languages are stored as display strings on the monster object. They are NOT added to the language-map. Creating 100 custom monsters with "Aquan" listed as a language does not add `:aquan` to the language-map. + +- **Race definitions:** Creating a custom race does NOT auto-create a language. The race builder lets you select from existing languages in the map. If you want a new language for your custom race, you must create it separately in the Language Builder. + +- **Ranger favored enemy feature:** The ranger `favored-enemy-option` function (`classes.cljc:1659-1704`) only consumes from `favored-enemy-types` / `humanoid-enemies` data and passes keys to `language-selection`. It does not inspect monster data, extract language strings, or dynamically create language entries. + +## Lifecycle: Pick → Remove Plugin → Re-add Plugin + +What happens when a player picks a language, the plugin providing it is removed, then re-added: + +1. **Pick:** Player selects "Aquan" from favored enemy languages. Character saves `::entity/key :aquan` in option tree, plus `modifiers/language :aquan` applies the proficiency. + +2. **Plugin removed:** Language-map no longer has `:aquan`. Fallback (layer 3) generates `{:name "Aquan" :key :aquan}`. Option `::key` is `:aquan` (from `name-to-kw "Aquan"`). Saved character still matches because `::entity/key :aquan` = generated option `::key :aquan`. + +3. **Plugin re-added:** Language-map has `:aquan` again. Layer 1 finds it. Plugin's definition is used instead of fallback. Option still matches because plugin likely names it "Aquan" too. + +**Edge case:** If the plugin uses a non-standard name like "Aquan (Water Primordial)", the option `::key` becomes `:aquan--water-primordial-`, which won't match the saved `:aquan`. The character would need to re-select the language. This is inherent to how the option matching system works, not specific to this fix. + +## Testing + +### Test file: `test/cljc/orcpub/dnd/e5/favored_enemy_language_test.cljc` + +6 tests, 384 assertions, 0 failures. + +| Test | What it verifies | +|------|-----------------| +| `test-language-lookup-fallback-never-returns-nil` | Known keys use map entry, unknown keys get generated fallback | +| `test-no-nil-in-favored-enemy-language-lookups` | Every key in `favored-enemy-types` resolves to non-nil | +| `test-no-nil-in-humanoid-enemy-language-lookups` | Every key in `humanoid-enemies` resolves to non-nil | +| `test-primoridial-typo-corrected` | Fey uses `:primordial`; legacy `:primoridial` resolves via shim | +| `test-homebrew-languages-used-when-available` | Plugin-defined languages are preferred over fallback | +| `test-key-to-name-generates-readable-names` | `key-to-name` converts keywords to readable display names | + +The test helper mirrors the fix logic: + +```clojure +(def known-corrections {:primoridial :primordial}) + +(defn lookup-with-fallback [lang-map k] + (or (lang-map k) + (lang-map (known-corrections k)) + {:name (opt5e/key-to-name k) :key k})) +``` + +This is duplicated (not referencing the private var) because `@#'ns/var` doesn't work in ClojureScript. The comment in the test file notes to keep it in sync. + +### Running the tests + +```bash +# ClojureScript tests (includes this test file) +lein doo phantom test once +``` + +## Implementation Files + +| File | What changed | +|------|-------------| +| `src/cljc/orcpub/dnd/e5/options.cljc:812-828` | `language-key-corrections` map + `language-selection` fallback | +| `src/cljc/orcpub/dnd/e5/options.cljc:3019` | Fixed `:primoridial` → `:primordial` in fey enemy type | +| `test/cljc/orcpub/dnd/e5/favored_enemy_language_test.cljc` | 6 behavioral tests | + +## Design Decisions + +### Why not remove exotic keys from `favored-enemy-types`? + +Keys like `:aquan`, `:gith`, `:bullywug` are legitimate D&D languages. Homebrew plugins define them. Removing the keys would mean players never see those language options, even when the appropriate plugin is loaded. The fallback approach preserves the full D&D language ecosystem while preventing nil corruption. + +### Why not remap exotic keys to base languages? + +Mapping `:aquan` → `:primordial` (its parent elemental language) would be semantically wrong. A player who picks "Elemental" as their favored enemy and gets "Aquan" as their language choice should get Aquan, not Primordial. The fallback generates the correct display name from the key. + +### Why a corrections map instead of just fixing the typo? + +Existing characters may have `:primoridial` saved in their option tree. The corrections map ensures these characters still resolve to "Primordial" instead of getting a generated fallback named "Primoridial" (with the typo visible to the user). New characters get `:primordial` (the typo is fixed in the source data), while old characters are silently corrected. + +### Why duplicate the corrections map in tests? + +Clojure's `@#'ns/private-var` deref syntax doesn't work in ClojureScript. Since the test file is `.cljc` (cross-platform), it can't access the private var. Duplicating is the pragmatic choice -- the map has one entry and changes rarely. + +## Related Documentation + +- [ORCBREW_FILE_VALIDATION.md](ORCBREW_FILE_VALIDATION.md) - Import/export validation +- [CONTENT_RECONCILIATION.md](CONTENT_RECONCILIATION.md) - Missing content detection +- [ERROR_HANDLING.md](ERROR_HANDLING.md) - Error handling patterns diff --git a/docs/ORCBREW_FILE_VALIDATION.md b/docs/ORCBREW_FILE_VALIDATION.md new file mode 100644 index 000000000..efadafa83 --- /dev/null +++ b/docs/ORCBREW_FILE_VALIDATION.md @@ -0,0 +1,550 @@ +# OrcBrew File Import/Export Validation + +## Overview + +This document explains the comprehensive validation system for `.orcbrew` files, which helps catch and fix bugs before they frustrate users. + +## What Changed? + +### Before +- ❌ Generic "Invalid .orcbrew file" errors +- ❌ No way to know what's wrong with a file +- ❌ One bad item breaks the entire import +- ❌ Bugs exported into files with no warning +- ❌ Silent failures and data loss + +### After +- ✅ Detailed error messages explaining what's wrong +- ✅ Progressive import (recovers valid items, skips invalid ones) +- ✅ Pre-export validation (catches bugs before they're saved) +- ✅ Automatic fixing of common corruption patterns +- ✅ Console logging for debugging + +## Features + +### 1. **Pre-Export Validation** + +Before creating an `.orcbrew` file, the system now validates your content: + +``` +Exporting "My Homebrew Pack"... +✓ Checking for nil values +✓ Checking for empty option-pack strings +✓ Validating data structure +✓ Running spec validation + +Export successful! +``` + +**If issues are found:** +- ⚠️ **Warnings** - File exports but issues are logged +- ❌ **Errors** - File won't export, must fix issues first + +**Check the browser console** (F12) for detailed information about any warnings or errors. + +### 2. **Enhanced Import Validation** + +When importing an `.orcbrew` file, you now get detailed feedback: + +#### **Successful Import** +``` +✅ Import successful + +Imported 25 items + +To be safe, export all content now to create a clean backup. +``` + +#### **Import with Warnings** +``` +⚠️ Import completed with warnings + +Imported: 23 valid items +Skipped: 2 invalid items + +Invalid items were skipped. Check the browser console for details. + +To be safe, export all content now to create a clean backup. +``` + +#### **Parse Error** +``` +⚠️ Could not read file + +Error: EOF while reading +Line: 45 + +The file may be corrupted or incomplete. Try exporting a +fresh copy if you have the original source. +``` + +#### **Validation Error** +``` +⚠️ Invalid orcbrew file + +Validation errors found: + • at spells > fireball: Missing required field: :option-pack + • at races > dwarf: Invalid value format + Got: {:name "Dwarf", :option-pack nil} + +To recover data from this file, you can: +1. Try progressive import (imports valid items, skips invalid ones) +2. Check the browser console for detailed validation errors +3. Export a fresh copy if you have the original source +``` + +### 3. **Progressive Import** + +The default import strategy is **progressive**, which means: + +- ✅ Valid items are imported successfully +- ⚠️ Invalid items are skipped and reported +- 📊 You get a count of imported vs skipped items +- 🔍 Detailed errors for skipped items appear in the console + +**Example:** + +If your file has 10 spells and 2 of them are missing the `option-pack` field: +- The 8 valid spells are imported +- The 2 invalid spells are skipped +- You see: "Imported: 8 valid items, Skipped: 2 invalid items" +- Console shows exactly which spells were skipped and why + +### 4. **Automatic Cleaning** + +The system automatically fixes these common corruption patterns: + +| Pattern | Fix | +|---------|-----| +| `disabled? nil` | `disabled? false` | +| `nil nil,` | (removed) | +| `:field-name nil` | (removed) | +| `option-pack ""` | `option-pack "Default Option Source"` | +| Empty plugin name `""` | `"Default Option Source"` | + +**This happens automatically** - you don't need to do anything! + +### 5. **Required Field Validation** + +The system validates that all content has required fields (like `:name`), with different behavior for import vs export: + +**On Import (Permissive):** +- Missing required fields are auto-filled with placeholder data +- Placeholder examples: `[Missing Name]`, `[Missing Trait Name]` +- Import continues without interruption +- Changes are logged for user awareness + +**On Export (Strict):** +- Missing fields trigger a warning modal +- Modal lists all items with issues +- User can: + - **Cancel**: Go back and fix the issues manually + - **Export Anyway**: Export with placeholder data filled in + +**Required fields by content type:** + +| Content Type | Required Fields | +|--------------|-----------------| +| Classes | `:name` | +| Subclasses | `:name` | +| Races | `:name` | +| Subraces | `:name` | +| Backgrounds | `:name` | +| Feats | `:name` | +| Spells | `:name`, `:level`, `:school` | +| Monsters | `:name` | +| Invocations | `:name` | +| Languages | `:name` | +| Traits (nested) | `:name` | + +### 6. **Unicode Normalization** + +Text content is automatically normalized to ASCII-safe characters. This prevents encoding issues with PDF generation and ensures compatibility across systems. + +**Characters automatically converted:** + +| Category | Examples | Converted To | +|----------|----------|--------------| +| Smart quotes | `'` `'` `"` `"` | `'` `"` | +| Dashes | `–` (en-dash) `—` (em-dash) | `-` `--` | +| Special spaces | non-breaking, thin, em | regular space | +| Symbols | `…` `•` `©` `®` `™` | `...` `*` `(c)` `(R)` `(TM)` | + +**Why this matters:** +- Smart quotes often sneak in from copy/paste from Word, Google Docs, etc. +- PDF fonts may not have glyphs for these characters +- Ensures clean exports that work everywhere + +**This happens during:** +- Import (after EDN parsing) +- Homebrew save (when you click Save) + +### 6. **Detailed Console Logging** + +Open the browser console (F12) to see: + +**On Export:** +```javascript +Export warnings for "My Pack": + Item spells/fireball has missing option-pack + Found nil value for key: :some-field +``` + +**On Import:** +```javascript +Import validation result: { + success: true, + imported_count: 23, + skipped_count: 2, + skipped_items: [ + {key: :invalid-spell, errors: "..."}, + {key: :bad-race, errors: "..."} + ] +} + +Skipped invalid items: + :invalid-spell + Errors: Missing required field: :option-pack + :bad-race + Errors: Invalid value format +``` + +## How to Use + +### Exporting Content + +**Single Plugin:** +1. Go to "Manage Homebrew" +2. Click "Export" next to your plugin +3. If warnings appear, check the console (F12) +4. Fix any issues and export again + +**All Plugins:** +1. Go to "Manage Homebrew" +2. Click "Export All" +3. If any plugin has errors, export is blocked +4. Check console for which plugin has issues +5. Fix issues and try again + +### Importing Content + +**Standard Import (Progressive):** +1. Click "Import Content" +2. Select your `.orcbrew` file +3. Read the import message +4. If warnings appear, check console for details +5. **IMPORTANT:** Export all content to create a clean backup + +**Strict Import (All-or-Nothing):** + +For users who want the old behavior (reject entire file if any item is invalid): + +*This feature is available via browser console only:* +```javascript +// In browser console +dispatch(['orcpub.dnd.e5.events/import-plugin-strict', 'Plugin Name', 'file contents']) +``` + +## Common Error Messages & Fixes + +### "Missing required field: :option-pack" + +**Cause:** Item doesn't have an `option-pack` field + +**Fix:** Each item MUST have `:option-pack ""` + +```clojure +;; Bad +{:name "Fireball" :level 3} + +;; Good +{:option-pack "My Homebrew" + :name "Fireball" + :level 3} +``` + +### "EOF while reading" + +**Cause:** File is incomplete or has unmatched brackets + +**Fix:** +1. Check for missing `}`, `]`, or `)` +2. Make sure every opening bracket has a closing bracket +3. If file is very corrupted, you may need to restore from backup + +### "Invalid value format" + +**Cause:** Value doesn't match expected format (e.g., number instead of string) + +**Fix:** Check the item in console logs to see which field is wrong + +### "File appears to be corrupted" + +**Cause:** File is incomplete, truncated, or contains invalid characters + +**Fix:** +1. Try re-exporting from the original source +2. If you only have this copy, try progressive import to recover what you can + +## Best Practices + +### 1. **Always Export After Importing** + +After importing any file: +``` +Import → Check Message → Export All → Save Backup +``` + +This creates a clean version with all auto-fixes applied. + +### 2. **Check Console Regularly** + +When working with homebrew content, keep the browser console open (F12) to catch issues early. + +### 3. **Keep Backups** + +Export your content regularly: +- After major changes +- After importing new content +- Before deleting or modifying existing content + +### 4. **Use Progressive Import for Recovery** + +If you have a corrupted file, progressive import can recover valid items: +- You'll lose invalid items, but keep everything else +- Console will show exactly what was lost +- You can manually recreate lost items + +### 5. **Fix Warnings Before Sharing** + +If you're sharing your homebrew: +- Export and check for warnings +- Fix any issues +- Export again to create a clean file +- Share the clean file + +## Technical Details + +### Validation Process + +1. **Auto-Clean** - Fix common corruption patterns +2. **Parse EDN** - Convert text to data structure +3. **Validate Structure** - Check against spec +4. **Item-Level Validation** - Check each item individually +5. **Report Results** - Show user-friendly messages + +### Import Strategies + +**Progressive (Default):** +- Imports valid items +- Skips invalid items +- Reports what was skipped +- Best for recovery + +**Strict (Optional):** +- All items must be valid +- Rejects entire file if any item is invalid +- Best for validation + +### Spec Requirements + +Every homebrew item must have: +- `:option-pack` (string) - The source/pack name +- Valid qualified keywords for content types +- Keywords must start with a letter +- Content types must be in `orcpub.dnd.e5` namespace + +### Error Data Structure + +All errors include: +- **error** - Error message +- **context** - Where the error occurred +- **hint** - Suggested fix (when available) +- **line** - Line number (for parse errors) + +## Developer Tools + +### Lein Prettify Tool + +A command-line tool for analyzing and debugging orcbrew files without running the full app: + +```bash +# Analyze a file for issues +lein with-profile +tools prettify-orcbrew path/to/file.orcbrew --analyze + +# Pretty-print a file (for manual inspection) +lein with-profile +tools prettify-orcbrew path/to/file.orcbrew + +# Write prettified output to file +lein with-profile +tools prettify-orcbrew path/to/file.orcbrew --output=pretty.edn +``` + +**The `+tools` profile skips Garden CSS compilation for faster startup.** + +**Analysis output includes:** +- File size +- `nil nil` pattern count +- Problematic Unicode characters (with counts by type) +- Unknown non-ASCII characters +- Disabled entry count +- Plugin structure (single vs multi-plugin) +- Traits missing `:name` fields + +**Example output:** +``` +=== Content Analysis === +File size: 1843232 bytes + +[WARNING] Found 36 'nil nil,' patterns + Spurious nil key-value pairs (e.g., {nil nil, :key :foo}) + +[WARNING] Found 1621 problematic Unicode characters (will be auto-fixed on import): + - right single quote (U+2019): 1598 occurrences + - left double quote (U+201C): 23 occurrences + +[INFO] Found 48 disabled entries (previously errored content) +``` + +## Troubleshooting + +### Import Fails with "Could not read file" + +**Likely Cause:** File is corrupted or incomplete + +**Solutions:** +1. Check file size - is it much smaller than expected? +2. Open in text editor - does it end abruptly? +3. Try progressive import to recover what you can +4. If you have the original source, re-export + +### Import Shows "Skipped X items" + +**Likely Cause:** Some items are invalid + +**Solutions:** +1. Check console (F12) for detailed errors +2. Note which items were skipped +3. Re-create those items manually +4. Export all content to save clean version + +### Export Blocked with Errors + +**Likely Cause:** Your content has invalid data + +**Solutions:** +1. Check console for which plugin has errors +2. Look for items with empty/nil option-pack +3. Fix the issues +4. Try export again + +### Console Shows "nil value" Warnings + +**Likely Cause:** Some fields have `nil` instead of proper values + +**Impact:** Usually auto-fixed, but may indicate data quality issues + +**Solutions:** +1. Review your content +2. Fill in missing fields +3. Export to create clean version + +## Related Features + +This validation system works alongside other import/export features: + +### Conflict Resolution +When duplicate keys are detected during import, the **Conflict Resolution** system helps you: +- Detect duplicate keys (same key in different sources) +- Choose how to handle each conflict (rename, skip, or replace) +- Automatically update references when renaming + +**See:** [CONFLICT_RESOLUTION.md](CONFLICT_RESOLUTION.md) + +### Missing Content Detection +After import, the **Content Reconciliation** system: +- Detects when characters reference content that isn't loaded +- Suggests similar content using fuzzy matching +- Helps identify which plugins are needed + +**See:** [CONTENT_RECONCILIATION.md](CONTENT_RECONCILIATION.md) + +### Error Handling Framework +All validation operations use the **Error Handling** framework: +- Structured error messages with `ex-info` +- Consistent logging and error reporting +- User-friendly error messages + +**See:** [ERROR_HANDLING.md](ERROR_HANDLING.md) + +### Required Fields +Understand what fields are needed for each content type: +- Spec requirements vs functional requirements +- Which fields can break features if missing +- Default values and optional fields + +**See:** [HOMEBREW_REQUIRED_FIELDS.md](HOMEBREW_REQUIRED_FIELDS.md) + +## Support + +If you encounter issues not covered here: + +1. **Check the console** (F12) for detailed errors +2. **Create an issue** on GitHub with: + - Error message from console + - Steps to reproduce + - Sample .orcbrew file (if you can share it) +3. **Try progressive import** to recover data + +## Migration Guide + +### For Existing Users + +Your existing `.orcbrew` files will continue to work! The system: + +1. Auto-cleans common issues +2. Uses progressive import by default +3. Helps you create cleaner files going forward + +**Recommended:** +1. Import your existing files +2. Check for any warnings +3. Export all content to create clean versions +4. Use the clean versions going forward + +### For Developers + +If you're building tools that generate `.orcbrew` files: + +1. **Use the validation API** before creating files +2. **Ensure all items have option-pack** +3. **Avoid nil values** in output +4. **Test with the validator** to catch issues early + +```clojure +;; Validate before export +(require '[orcpub.dnd.e5.import-validation :as import-val]) + +(let [result (import-val/validate-before-export my-plugin)] + (if (:valid result) + (export-to-file my-plugin) + (handle-errors (:errors result)))) +``` + +## Version History + +### v2.0 - Comprehensive Validation (Current) +- Added pre-export validation +- Added progressive import +- Added detailed error messages +- Added automatic cleaning +- Added console logging +- 30+ validation test cases + +### v1.0 - Basic Validation (Legacy) +- Simple spec validation +- All-or-nothing import +- Generic error messages + +--- + +**Questions?** Open an issue on GitHub or check the browser console for detailed error information. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..76590fd3a --- /dev/null +++ b/docs/README.md @@ -0,0 +1,106 @@ +# OrcPub Documentation + +Guides for developers and power users working with OrcPub's homebrew content system. + +## Quick Navigation + +**For Users:** +- [📥 Import/Export Validation](ORCBREW_FILE_VALIDATION.md) - Safely import/export `.orcbrew` files +- [⚔️ Conflict Resolution](CONFLICT_RESOLUTION.md) - Handle duplicate keys during import +- [🔍 Missing Content Detection](CONTENT_RECONCILIATION.md) - Find/fix missing content references +- [📋 Required Fields Guide](HOMEBREW_REQUIRED_FIELDS.md) - Required fields per content type + +**For Developers:** +- [🚨 Error Handling](ERROR_HANDLING.md) - Error handling utilities +- [🗡️ Language Selection Fix](LANGUAGE_SELECTION_FIX.md) - Ranger favored enemy language corruption (#296) +- [🐳 Docker User Management](docker-user-management.md) - Verified user setup for Docker deployments + +## Key Design Decisions + +### Why Progressive Import? + +**Problem:** Users had partially corrupted `.orcbrew` files. Previous all-or-nothing approach: one bad item blocks entire import. + +**Decision:** Import valid items, skip invalid, show detailed error report. + +**Rationale:** Partial data recovery better than total failure. Users can fix issues incrementally. + +→ [ORCBREW_FILE_VALIDATION.md](ORCBREW_FILE_VALIDATION.md) + +### Why Interactive Conflict Resolution? + +**Problem:** Silent overwrites caused data loss. Users wouldn't notice until characters broke. + +**Decision:** Detect conflicts pre-import, show modal with resolution options (rename/skip/replace). + +**Critical insight:** When renaming parent content (e.g., class), all child references (subclasses) must auto-update or they become orphaned. Early implementation forgot this → orphaned subclasses appeared in UI but were unselectable. + +→ [CONFLICT_RESOLUTION.md](CONFLICT_RESOLUTION.md) + +### Why Fuzzy Matching for Missing Content? + +**Problem:** Content keys change between versions (`:blood-hunter` → `:blood-hunter-v2`). Users see "(not loaded)" with no help. + +**Decision:** Multiple fuzzy matching strategies (Levenshtein, prefix, name similarity) to catch typos and versioning. + +**Gotcha:** Must exclude built-in content (PHB, Xanathar's) or system suggests switching from homebrew Artificer to PHB Artificer (which doesn't exist in 5e). + +→ [CONTENT_RECONCILIATION.md](CONTENT_RECONCILIATION.md) + +### Why a Fallback Chain for Language Selection? + +**Problem:** Ranger favored enemy types reference 24 exotic language keys (`:aquan`, `:gith`, `:bullywug`, etc.) that aren't in the base 16 languages. `language-selection` returned nil for these, corrupting character data. + +**Decision:** Three-layer fallback: language-map → corrections shim → generated entry from key. Never returns nil. + +**Critical insight:** Can't remove or remap exotic keys because homebrew plugins legitimately define them. The fallback generates a valid entry when the plugin isn't loaded and uses the plugin's definition when it is. + +**Gotcha:** Two different "key" concepts exist: language `:key` (data keyword like `:aquan`) and option `::entity/key` (derived from display name via `name-to-kw`). The fallback must produce names that round-trip correctly through `key-to-name` / `name-to-kw`. + +> [LANGUAGE_SELECTION_FIX.md](LANGUAGE_SELECTION_FIX.md) + +**Problem:** Inconsistent error handling across codebase. Some code logged, some didn't. User messages inconsistent. + +**Decision:** Centralize in macros (`with-db-error-handling`, `with-email-error-handling`, `with-validation`). + +**Rationale:** Consistency in logging, user messages, error data structure. Easier to add monitoring later. + +→ [ERROR_HANDLING.md](ERROR_HANDLING.md) + +## Common Workflows + +**Creating homebrew:** Create in UI → Export → Check console warnings → Fix required fields → Re-export + +**Importing content:** Import file → Resolve conflicts (if any) → Check for missing content warnings + +**Debugging imports:** Console (F12) → Check validation errors → Use progressive import to recover partial data + +**Fixing characters:** Check missing content warnings → Import plugin or use suggested alternative + +## Known Limitations + +**Field requirements:** Not all required fields are enforced. Some will silently break features (see HOMEBREW_REQUIRED_FIELDS.md). + +**Batch operations:** Can only import one file at a time. Multi-file import with cross-reference resolution would be valuable. + +## Implementation Files + +**Import/Export:** `import_validation.cljs`, `events.cljs` +**Import UI:** `views/import_log.cljs` (log panel with grouped collapsible sections), `views/conflict_resolution.cljs` (conflict modal, export warning) +**Content Reconciliation:** `content_reconciliation.cljs`, `subs.cljs`, `character_builder.cljs` (warning UI) +**Error Handling:** `errors.cljc` (DRY macros) +**Tests:** `import_validation_test.cljs`, `errors_test.clj`, `favored_enemy_language_test.cljc` + +All in `src/cljs/orcpub/dnd/e5/` unless noted. + +## Debugging Tips + +**Import failures:** Check console (F12) → Use progressive import to recover partial data + +**Character broken:** Look for "(not loaded)" warnings → Import missing plugin or use suggested alternative + +**Conflicts on import:** Modal should appear automatically → Choose rename/skip/replace per item + +--- + +**Branch:** `feature/error-handling-import-validation` | **Last updated:** 2026-02-19 diff --git a/docs/docker-user-management.md b/docs/docker-user-management.md new file mode 100644 index 000000000..eb2a1908e --- /dev/null +++ b/docs/docker-user-management.md @@ -0,0 +1,205 @@ +# Docker User Management + +Dungeon Master's Vault uses email verification for new accounts by default. When self-hosting with Docker, you likely don't have (or want) an SMTP server configured. These scripts let you create pre-verified user accounts directly in the Datomic database, bypassing the email verification step entirely. + +## Quick Start (Without Cloning) + +You don't need to clone the whole repo. Grab the three files you need and go: + +```bash +# 1. Get the compose file and user scripts +mkdir orcpub && cd orcpub +curl -fLO https://raw.githubusercontent.com/Orcpub/orcpub/develop/docker-compose.yaml +curl -fLO https://raw.githubusercontent.com/Orcpub/orcpub/develop/docker-user.sh +mkdir -p docker/scripts +curl -fL https://raw.githubusercontent.com/Orcpub/orcpub/develop/docker/scripts/manage-user.clj \ + -o docker/scripts/manage-user.clj +chmod +x docker-user.sh + +# 2. Start the stack +docker compose up -d + +# 3. Create your account (script waits for healthcheck automatically) +./docker-user.sh create myname me@email.com MyPassword123 +``` + +Open https://localhost and log in. That's it. + +### Day-to-day + +```bash +docker compose up -d # start (if not already running) +docker compose down # stop +docker compose down -v # stop and wipe data +``` + +User management works any time the stack is up: + +```bash +./docker-user.sh list # who's in there? +./docker-user.sh create buddy friend@email.com TheirPass456 # add someone +./docker-user.sh batch players.txt # add many at once +./docker-user.sh check buddy # look up a user +``` + +### The script handles the waiting + +You don't need to know when Datomic is "ready." If you run `docker compose up -d` and immediately run `./docker-user.sh create ...`, it will: + +1. Find the orcpub container automatically +2. Poll the Docker healthcheck (dots printed while waiting) +3. Run the command once healthy + +No "wait 30 seconds then..." — just run it. + +If you forget the commands: `./docker-user.sh --help` + +## Full Setup (With Cloned Repo) + +If you've cloned the repo, the setup script generates secure passwords, SSL certs, and required directories in one step: + +```bash +./docker-setup.sh # Interactive — prompts for optional values +./docker-setup.sh --auto # Non-interactive — accepts all defaults +./docker-setup.sh --auto --force # Regenerate everything from scratch +``` + +In interactive mode, the setup script will prompt for an initial admin account. Then start the stack and initialize: + +```bash +docker compose up -d +./docker-user.sh init # creates the admin user configured in .env +``` + +Or skip the prompt and create users directly: + +```bash +docker compose up -d +./docker-user.sh create admin admin@example.com MySecurePass123 +``` + +The setup script creates a `.env` file used by both `docker-compose.yaml` and `docker-compose-build.yaml`. You can also copy and edit `.env.example` manually if you prefer. + +### Environment Variables + +| Variable | Description | Default | +|---|---|---| +| `PORT` | Application port | `8890` | +| `ADMIN_PASSWORD` | Datomic admin interface password | generated | +| `DATOMIC_PASSWORD` | Datomic application password | generated | +| `SIGNATURE` | JWT signing secret (20+ chars) | generated | +| `EMAIL_SERVER_URL` | SMTP server (leave empty to skip email) | empty | +| `EMAIL_ACCESS_KEY` | SMTP username | empty | +| `EMAIL_SECRET_KEY` | SMTP password | empty | +| `EMAIL_SERVER_PORT` | SMTP port | `587` | +| `EMAIL_FROM_ADDRESS` | Sender address | empty | +| `EMAIL_SSL` / `EMAIL_TLS` | SMTP encryption | `FALSE` | +| `INIT_ADMIN_USER` | Initial admin username (for `./docker-user.sh init`) | empty | +| `INIT_ADMIN_EMAIL` | Initial admin email | empty | +| `INIT_ADMIN_PASSWORD` | Initial admin password | empty | + +## User Management Commands + +### Initialize Admin from .env + +```bash +./docker-user.sh init +``` + +Reads `INIT_ADMIN_USER`, `INIT_ADMIN_EMAIL`, and `INIT_ADMIN_PASSWORD` from `.env` and creates the account. Safe to run multiple times — if the user already exists, it's skipped. + +This is the easiest path after running `docker-setup.sh` in interactive mode, which prompts for these values. + +### Create a Single User + +```bash +./docker-user.sh create +``` + +Creates a new user account that is **automatically verified** — no email confirmation needed. The password is hashed with bcrypt before storage. + +### Create Multiple Users (Batch) + +```bash +cp docker/scripts/users.example.txt users.txt +# edit users.txt with your actual users +./docker-user.sh batch users.txt +``` + +Creates multiple users from a local file in a single JVM session (much faster than calling `create` repeatedly). The script copies the file into the container automatically — you don't need to interact with Docker directly. A template is included at [`docker/scripts/users.example.txt`](../docker/scripts/users.example.txt). File format: + +``` +# Comments and blank lines are ignored +admin admin@example.com SecurePass123 +player1 player1@example.com AnotherPass456 +player2 player2@example.com YetAnotherPass789 +``` + +Duplicates are logged and skipped (not treated as errors). + +### Verify an Existing User + +```bash +./docker-user.sh verify +``` + +If a user registered through the web UI but never received/clicked the verification email, this marks them as verified. + +### Check a User + +```bash +./docker-user.sh check +``` + +Displays the user's username, email, verification status, and creation date. + +### List All Users + +```bash +./docker-user.sh list +``` + +Prints a table of all users in the database with their verification status. + +### Options + +| Flag | Description | +|---|---| +| `--container ` | Override automatic container detection | +| `--help` | Show usage information | + +## Docker Compose Changes + +Both `docker-compose.yaml` and `docker-compose-build.yaml` have been updated: + +- **Environment variables use `.env` interpolation** — no more editing passwords directly in the YAML. All config flows from the `.env` file generated by `docker-setup.sh`. +- **Native healthchecks** — Datomic and the application containers declare healthchecks so that dependent services wait for readiness automatically. This replaces fragile startup-order workarounds. +- **Service dependencies use `condition: service_healthy`** — nginx won't start until the app is actually serving, and the app won't start until Datomic is accepting connections. + +## How It Works + +The user management script ([docker/scripts/manage-user.clj](../docker/scripts/manage-user.clj)) runs as a Clojure program inside the orcpub container, reusing the application's own uberjar classpath. This gives it direct access to `datomic.api` and `buddy.hashers` without installing anything extra. + +The shell wrapper (`docker-user.sh`) handles: +1. Auto-detecting the running orcpub container +2. Waiting for Docker's native healthcheck to report healthy +3. Copying the Clojure script into the container +4. Executing it via `java -cp /orcpub.jar clojure.main` + +## Troubleshooting + +**"Cannot find the orcpub container"** +- Ensure containers are running: `docker compose ps` +- If using a non-standard project name, pass `--container ` explicitly + +**"Cannot connect to Datomic"** +- The datomic container may still be starting. The script waits up to 120 seconds, but on slow systems it may need longer. +- Check datomic logs: `docker compose logs datomic` + +**"Container reported unhealthy"** +- Check the application logs: `docker compose logs orcpub` +- Verify `.env` values match between `DATOMIC_PASSWORD` and the password in `DATOMIC_URL` + +**User created but can't log in** +- Run `./docker-user.sh check ` to confirm the account exists and is verified +- Make sure `SIGNATURE` in `.env` hasn't changed since the container started (restart containers after `.env` changes) diff --git a/docs/email-system.md b/docs/email-system.md new file mode 100644 index 000000000..579dc331f --- /dev/null +++ b/docs/email-system.md @@ -0,0 +1,155 @@ +# Email System + +Overview of email-related flows, schema, configuration, and behavior. + +## Configuration + +All email is sent via [postal](https://github.com/drewr/postal) using SMTP credentials from environment variables: + +| Env var | Purpose | +|---------|---------| +| `EMAIL_ACCESS_KEY` | SMTP username | +| `EMAIL_SECRET_KEY` | SMTP password | +| `EMAIL_SERVER_URL` | SMTP host | +| `EMAIL_SERVER_PORT` | SMTP port (default `587`) | +| `EMAIL_SSL` | Enable SSL (`true`/`false`) | +| `EMAIL_TLS` | Enable TLS (`true`/`false`) | +| `EMAIL_FROM_ADDRESS` | Sender address (default `no-reply@orcpub.com`) | +| `EMAIL_ERRORS_TO` | Address for error notification emails (optional) | + +Configuration is read at send-time by `email/email-cfg` (`src/clj/orcpub/email.clj`). + +## Schema + +User attributes related to email and verification (`src/clj/orcpub/db/schema.clj`): + +| Attribute | Type | Purpose | +|-----------|------|---------| +| `:orcpub.user/email` | string | Confirmed email address | +| `:orcpub.user/pending-email` | string | Requested new email (awaiting verification) | +| `:orcpub.user/verified?` | boolean | Whether the user has verified their email | +| `:orcpub.user/verification-key` | string | UUID used in verification links | +| `:orcpub.user/verification-sent` | instant | When the verification email was sent | +| `:orcpub.user/password-reset-key` | string | UUID used in password reset links | +| `:orcpub.user/password-reset-sent` | instant | When the password reset email was sent | +| `:orcpub.user/password-reset` | instant | When the password was actually reset | + +**Note:** `:orcpub.user/email` has no uniqueness constraint in the schema. Uniqueness is enforced at the application level via `email-query`. See the known issues section. + +## Flows + +### 1. Registration Verification + +**Trigger:** `POST /register` (via `routes/register`) + +1. Validate username, email, password +2. Check email/username not already taken (`email-query`, `username-query`) +3. Create user entity with `verified? false`, generate `verification-key`, set `verification-sent` +4. Send registration verification email (`email/send-verification-email`) +5. User clicks link → `GET /verify?key=...` → `routes/verify` + +**Verify behavior (registration path):** +- If already verified and no `pending-email` → redirect to success +- If `verification-sent` is nil or expired (24h) → redirect to failed +- Otherwise → set `verified? true`, redirect to success + +**Re-verify:** `GET /re-verify?email=...` (`routes/re-verify`) re-sends the verification email for unverified accounts. + +**Login gate:** Unverified users cannot log in. If the verification has expired, the login error tells them to re-register. + +**Files:** `routes.clj:register`, `routes.clj:do-verification`, `routes.clj:verify`, `email.clj:send-verification-email` + +### 2. Email Change + +**Trigger:** `PUT /user/email` (via `routes/request-email-change`, requires auth) + +1. Validate new email (format, not same as current, not already taken) +2. Check rate limit (see Rate Limiting below) +3. Store `pending-email`, generate new `verification-key`, set `verification-sent` +4. Send email-change verification to the **new** address (`email/send-email-change-verification`) +5. If send fails → full rollback (retract `pending-email`, `verification-key`, `verification-sent`), return 500 + +**Verify behavior (email-change path):** +- If expired (24h) → retract `pending-email`, `verification-key`, `verification-sent`; redirect to failed +- If `pending-email` exists → re-check email availability (race-condition guard): + - If email was claimed by another user since request → retract all pending state, redirect to failed + - Otherwise → swap `email` to `pending-email`, retract `pending-email`, `verification-key`, `verification-sent`; redirect to success +- Key is invalidated after use (retracted) — link cannot be reused + +**Free resend:** Within 1–5 minutes of the original request, resending the same email re-uses the existing `verification-key` and does not update `verification-sent` (no rolling window). See Rate Limiting. + +**Files:** `routes.clj:request-email-change`, `routes.clj:verify` (pending-email branch), `email.clj:send-email-change-verification`, `events.cljs:change-email`, `views.cljs:my-account-page` + +### 3. Password Reset + +**Trigger:** `GET /send-password-reset?email=...` (via `routes/send-password-reset`) + +1. Look up user by email +2. Generate `password-reset-key`, set `password-reset-sent` +3. Send password reset email (`email/send-reset-email`) + +**Reset behavior:** `POST /reset-password` (via `routes/reset-password`) +- Validates new password and password match +- Sets new password hash, sets `password-reset` timestamp, sets `verified? true` + +**Expiration:** `password-reset-expired?` checks if `password-reset-sent` is older than 24 hours. + +**Files:** `routes.clj:send-password-reset`, `routes.clj:do-send-password-reset`, `routes.clj:reset-password`, `email.clj:send-reset-email` + +### 4. Error Notification + +**Trigger:** Called from exception handlers (e.g., Pedestal error interceptor) + +- Sends a plaintext email with the request context and exception data +- Only sends if `EMAIL_ERRORS_TO` is set +- Uses `email/send-error-email` + +**Files:** `email.clj:send-error-email` + +## Rate Limiting (Email Change) + +Rate limiting is enforced by `routes/email-change-rate-limited?` based on `verification-sent` and whether the request is a resend (same email as `pending-email`). + +Three zones measured from `verification-sent`: + +``` +0 ──────── 1 min ──────── 5 min ──────── ∞ +│ BLOCKED │ FREE RESEND │ OPEN │ +│ (transit) │ (same email) │ (any email) │ +│ │ blocked for │ │ +│ │ diff email │ │ +``` + +- **0–1 min:** All requests blocked. Email is in transit. Client shows "Your email is on its way. You can resend in N seconds." +- **1–5 min:** Resend of same email allowed (free resend, no DB write, reuses existing key). Different email blocked. Client shows "Please wait N minutes before requesting another change." +- **5+ min:** Any request allowed. New `verification-key` generated, `verification-sent` updated. + +The 429 response includes `retry-after-secs` so the client can display a specific countdown. + +## Expiration Windows + +| Window | Duration | Function | +|--------|----------|----------| +| Verification link | 24 hours | `verification-expired?` | +| Password reset link | 24 hours | `password-reset-expired?` | +| Email change rate limit | 5 minutes | `email-change-rate-limited?` | +| Free resend grace | 1–5 minutes | `email-change-rate-limited?` + free resend branch | + +## File Map + +| File | Role | +|------|------| +| `src/clj/orcpub/email.clj` | Email templates and send functions (postal) | +| `src/clj/orcpub/routes.clj` | Server handlers: register, verify, email change, password reset | +| `src/clj/orcpub/db/schema.clj` | Datomic schema for user attributes | +| `src/cljc/orcpub/route_map.cljc` | Route definitions (shared server/client) | +| `src/cljs/orcpub/dnd/e5/events.cljs` | Re-frame events for email change UI | +| `src/cljs/orcpub/dnd/e5/views.cljs` | My Account page with email change form | +| `src/cljs/orcpub/dnd/e5/subs.cljs` | Subscriptions for pending-email, email-change state | +| `test/clj/orcpub/email_change_test.clj` | Email change tests (11 tests, datomock) | + +## Known Issues + +- **No uniqueness constraint on email in schema.** Uniqueness is enforced at the application level by `email-query` (at request time) and a race-condition guard (at verify time). A Datomic `:db.unique/value` constraint on `:orcpub.user/email` would be the proper fix but requires a data migration to handle any existing duplicates. + +- **Pending-email conflicts not checked.** Two users can simultaneously request the same new email. Both receive verification emails, but only the first to verify succeeds — the second is caught by the race-condition guard. The "loser" gets a confusing failure after clicking a valid-looking link. diff --git a/env/dev/env/index.cljs b/env/dev/env/index.cljs index 17a255138..885bd1089 100644 --- a/env/dev/env/index.cljs +++ b/env/dev/env/index.cljs @@ -5,6 +5,6 @@ (set! js/window.goog js/undefined) (-> (js/require "figwheel-bridge") - (.withModules #js {"react" (js/require "react"), "react-native" (js/require "react-native"), "expo" (js/require "expo"), "./assets/icons/app.png" (js/require "../../assets/icons/app.png"), "./assets/icons/loading.png" (js/require "../../assets/icons/loading.png"), "./assets/images/cljs.png" (js/require "../../assets/images/cljs.png"), "./assets/images/orcpub-logo.svg" (js/require "../../assets/images/orcpub-logo.svg"), "./assets/images/orcpub-logo.png" (js/require "../../assets/images/orcpub-logo.png")} + (.withModules #js {"react" (js/require "react"), "react-native" (js/require "react-native"), "expo" (js/require "expo"), "./assets/icons/app.png" (js/require "../../assets/icons/app.png"), "./assets/icons/loading.png" (js/require "../../assets/icons/loading.png"), "./assets/images/cljs.png" (js/require "../../assets/images/cljs.png"), "./assets/images/dmv-logo.svg" (js/require "../../assets/images/dmv-logo.svg"), "./assets/images/dmv-logo.png" (js/require "../../assets/images/dmv-logo.png")} ) (.start "main")) diff --git a/exp.json b/exp.json deleted file mode 100644 index 052401d96..000000000 --- a/exp.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "name": "orcpub", - "description": "No description", - "slug": "orcpub", - "sdkVersion": "17.0.0", - "version": "1.0.0", - "orientation": "portrait", - "primaryColor": "#cccccc", - "privacy": "public", - "icon": "./assets/icons/app.png", - "notification": { - "icon": "./assets/icons/loading.png", - "color": "#000000" - }, - "loading": { - "icon": "https://s3.amazonaws.com/exp-brand-assets/ExponentEmptyManifest_192.png", - "hideExponentText": false - }, - "packagerOpts": { - "assetExts": ["ttf","otf"], - "nonPersistent": "" - }, - "ios": { - "supportsTablet": true - } -} diff --git a/lib/datomic-free-0.9.5703.tar.gz b/lib/datomic-free-0.9.5703.tar.gz new file mode 100644 index 000000000..7cf50ec3c Binary files /dev/null and b/lib/datomic-free-0.9.5703.tar.gz differ diff --git a/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/_remote.repositories b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/_remote.repositories new file mode 100644 index 000000000..a0c22e2ed --- /dev/null +++ b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/_remote.repositories @@ -0,0 +1,6 @@ +#NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice. +#Tue Jan 29 13:44:11 MST 2019 +fontbox-2.1.0-20170324.165924-846.jar>apache= +fontbox-2.1.0-20170324.165924-846.pom>= +fontbox-2.1.0-20170324.165924-846.jar>= +fontbox-2.1.0-20170324.165924-846.pom>apache= diff --git a/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-20170324.165924-846.jar b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-20170324.165924-846.jar new file mode 100644 index 000000000..c9eed7e30 Binary files /dev/null and b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-20170324.165924-846.jar differ diff --git a/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-20170324.165924-846.jar.sha1 b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-20170324.165924-846.jar.sha1 new file mode 100644 index 000000000..a6b8698f1 --- /dev/null +++ b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-20170324.165924-846.jar.sha1 @@ -0,0 +1 @@ +5578ca82a5bf8f05ec109280748be69783486628 \ No newline at end of file diff --git a/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-20170324.165924-846.pom b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-20170324.165924-846.pom new file mode 100644 index 000000000..bc1aa07f5 --- /dev/null +++ b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-20170324.165924-846.pom @@ -0,0 +1,71 @@ + + + + 4.0.0 + + + org.apache.pdfbox + pdfbox-parent + 2.1.0-SNAPSHOT + ../parent/pom.xml + + + fontbox + bundle + + Apache FontBox + + The Apache FontBox library is an open source Java tool to obtain low level information + from font files. FontBox is a subproject of Apache PDFBox. + + + 2008 + http://pdfbox.apache.org/ + + + + commons-logging + commons-logging + + + junit + junit + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.rat + apache-rat-plugin + + + src/main/resources/org/apache/fontbox/cmap/* + + + + + + + + diff --git a/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-20170324.165924-846.pom.sha1 b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-20170324.165924-846.pom.sha1 new file mode 100644 index 000000000..00462c663 --- /dev/null +++ b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-20170324.165924-846.pom.sha1 @@ -0,0 +1 @@ +40456f8a00684c3fecc8d327597ee355e909630b \ No newline at end of file diff --git a/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-SNAPSHOT.jar b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-SNAPSHOT.jar new file mode 100644 index 000000000..c9eed7e30 Binary files /dev/null and b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-SNAPSHOT.jar differ diff --git a/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-SNAPSHOT.pom b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-SNAPSHOT.pom new file mode 100644 index 000000000..bc1aa07f5 --- /dev/null +++ b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/fontbox-2.1.0-SNAPSHOT.pom @@ -0,0 +1,71 @@ + + + + 4.0.0 + + + org.apache.pdfbox + pdfbox-parent + 2.1.0-SNAPSHOT + ../parent/pom.xml + + + fontbox + bundle + + Apache FontBox + + The Apache FontBox library is an open source Java tool to obtain low level information + from font files. FontBox is a subproject of Apache PDFBox. + + + 2008 + http://pdfbox.apache.org/ + + + + commons-logging + commons-logging + + + junit + junit + + + + + + + org.apache.felix + maven-bundle-plugin + true + + + org.apache.rat + apache-rat-plugin + + + src/main/resources/org/apache/fontbox/cmap/* + + + + + + + + diff --git a/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/maven-metadata-.xml.sha1 b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/maven-metadata-.xml.sha1 new file mode 100644 index 000000000..173c36ba9 --- /dev/null +++ b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/maven-metadata-.xml.sha1 @@ -0,0 +1 @@ +dbfd29b44094c4536436c16288d928966d6af89c diff --git a/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/maven-metadata-apache.snapshots.xml.sha1 b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/maven-metadata-apache.snapshots.xml.sha1 new file mode 100644 index 000000000..173c36ba9 --- /dev/null +++ b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/maven-metadata-apache.snapshots.xml.sha1 @@ -0,0 +1 @@ +dbfd29b44094c4536436c16288d928966d6af89c diff --git a/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/maven-metadata-apache.xml b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/maven-metadata-apache.xml new file mode 100644 index 000000000..f556a8f49 --- /dev/null +++ b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/maven-metadata-apache.xml @@ -0,0 +1,31 @@ + + + org.apache.pdfbox + fontbox + 2.1.0-SNAPSHOT + + + 20170324.165924 + 846 + + 20170325065546 + + + sources + jar + 2.1.0-20170324.165924-846 + 20170324165924 + + + jar + 2.1.0-20170324.165924-846 + 20170324165924 + + + pom + 2.1.0-20170324.165924-846 + 20170324165924 + + + + diff --git a/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/maven-metadata-apache.xml.sha1 b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/maven-metadata-apache.xml.sha1 new file mode 100644 index 000000000..173c36ba9 --- /dev/null +++ b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/maven-metadata-apache.xml.sha1 @@ -0,0 +1 @@ +dbfd29b44094c4536436c16288d928966d6af89c diff --git a/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/resolver-status.properties b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/resolver-status.properties new file mode 100644 index 000000000..b3181c66d --- /dev/null +++ b/lib/org/apache/pdfbox/fontbox/2.1.0-SNAPSHOT/resolver-status.properties @@ -0,0 +1,11 @@ +#NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice. +#Mon Nov 15 12:10:30 CST 2021 +maven-metadata-apache.snapshots.xml.error= +maven-metadata-.xml.error= +maven-metadata-clojars.xml.error= +maven-metadata-my.datomic.com.xml.error=Could not transfer metadata org.apache.pdfbox\:fontbox\:2.1.0-SNAPSHOT/maven-metadata.xml from/to my.datomic.com (https\://my.datomic.com/repo)\: Not authorized , ReasonPhrase\:Unauthorized. +maven-metadata-my.datomic.com.xml/@default-my.datomic.com-https\://my.datomic.com/repo/.lastUpdated=1636999830024 +maven-metadata-apache.xml.lastUpdated=1583301979070 +maven-metadata-clojars.xml.lastUpdated=1636999829901 +maven-metadata-.xml.lastUpdated=1636999830390 +maven-metadata-apache.snapshots.xml.lastUpdated=1636999830128 diff --git a/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/_remote.repositories b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/_remote.repositories new file mode 100644 index 000000000..398b43fa2 --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/_remote.repositories @@ -0,0 +1,4 @@ +#NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice. +#Tue Jan 29 13:44:05 MST 2019 +pdfbox-parent-2.1.0-20170324.165845-499.pom>apache= +pdfbox-parent-2.1.0-20170324.165845-499.pom>= diff --git a/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/maven-metadata-.xml.sha1 b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/maven-metadata-.xml.sha1 new file mode 100644 index 000000000..90d20509f --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/maven-metadata-.xml.sha1 @@ -0,0 +1 @@ +2c81d996339b62d06424c3b5813fa6825307ca77 diff --git a/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/maven-metadata-apache.snapshots.xml.sha1 b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/maven-metadata-apache.snapshots.xml.sha1 new file mode 100644 index 000000000..90d20509f --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/maven-metadata-apache.snapshots.xml.sha1 @@ -0,0 +1 @@ +2c81d996339b62d06424c3b5813fa6825307ca77 diff --git a/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/maven-metadata-apache.xml b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/maven-metadata-apache.xml new file mode 100644 index 000000000..9b39f1fc5 --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/maven-metadata-apache.xml @@ -0,0 +1,20 @@ + + + org.apache.pdfbox + pdfbox-parent + 2.1.0-SNAPSHOT + + + 20170324.165845 + 499 + + 20170324165845 + + + pom + 2.1.0-20170324.165845-499 + 20170324165845 + + + + diff --git a/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/maven-metadata-apache.xml.sha1 b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/maven-metadata-apache.xml.sha1 new file mode 100644 index 000000000..90d20509f --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/maven-metadata-apache.xml.sha1 @@ -0,0 +1 @@ +2c81d996339b62d06424c3b5813fa6825307ca77 diff --git a/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/pdfbox-parent-2.1.0-20170324.165845-499.pom b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/pdfbox-parent-2.1.0-20170324.165845-499.pom new file mode 100644 index 000000000..8f476cd3c --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/pdfbox-parent-2.1.0-20170324.165845-499.pom @@ -0,0 +1,378 @@ + + + + + + 4.0.0 + + + org.apache + apache + 16 + + + + org.apache.pdfbox + pdfbox-parent + 2.1.0-SNAPSHOT + pom + + PDFBox parent + 2002 + + + The Apache Software Foundation + http://pdfbox.apache.org + + + + jira + https://issues.apache.org/jira/browse/PDFBOX + + + + UTF-8 + UTF-8 + + + + 3.0.0 + + + + + + junit + junit + 4.12 + test + + + commons-logging + commons-logging + 1.2 + + + commons-io + commons-io + 2.4 + test + + + org.bouncycastle + bcprov-jdk15on + 1.55 + + + org.bouncycastle + bcmail-jdk15on + 1.55 + + + org.bouncycastle + bcpkix-jdk15on + 1.55 + + + log4j + log4j + 1.2.17 + + + + + com.levigo.jbig2 + levigo-jbig2-imageio + 1.6.5 + test + + + com.github.jai-imageio + jai-imageio-core + 1.3.1 + test + + + com.github.jai-imageio + jai-imageio-jpeg2000 + 1.3.0 + test + + + + + + + pedantic + + + + org.apache.rat + apache-rat-plugin + + + verify + + check + + + + + + + + + + + + + maven-compiler-plugin + + true + 1.6 + 1.6 + UTF-8 + + + + maven-javadoc-plugin + + 1.6 + + http://download.oracle.com/javase/1.6.0/docs/api/ + + UTF-8 + true + + + + maven-release-plugin + + false + deploy + -Papache-release,pedantic + true + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + true + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + 1.14 + + + check-java-version + test + + check + + + + org.codehaus.mojo.signature + java16 + 1.0 + + + + + + + + + + org.apache.rat + apache-rat-plugin + 0.11 + + + release.properties + + + + + org.apache.felix + maven-bundle-plugin + + 2.5.4 + + + + + + + + + lehmi + Andreas Lehmkühler + + PMC Chair + + + + adam + Adam Nichols + + PMC Member + + + + blitchfield + Ben Litchfield + + PMC Member + + + + carrier + Brian Carrier + + PMC Member + + + + danielwilson + Daniel Wilson + + PMC Member + + + + gbailleul + Guillaume Bailleul + + PMC Member + + + + jeremias + Jeremias Maerki + + PMC Member + + + + koch + Johannes Koch + + PMC Member + + + + jahewson + John Hewson + + PMC Member + + + + kjackson + Kevin Jackson + + PMC Member + + + + msayhoun + Maruan Sayhoun + + PMC Member + + + + pkoch + Phillipp Koch + + PMC Member + + + + tallison + Tim Allison + + PMC Member + + + + tchojecki + Thomas Chojecki + + PMC Member + + + + tboehme + Timo Boehme + + PMC Member + + + + tilman + Tilman Hausherr + + PMC Member + + + + vfed + Villu Ruusmann + + PMC Member + + + + leleueri + Eric Leleu + + Emeritus PMC Member + + + + jukka + Jukka Zitting + + Emeritus PMC Member + + + + diff --git a/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/pdfbox-parent-2.1.0-20170324.165845-499.pom.sha1 b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/pdfbox-parent-2.1.0-20170324.165845-499.pom.sha1 new file mode 100644 index 000000000..8372bd402 --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/pdfbox-parent-2.1.0-20170324.165845-499.pom.sha1 @@ -0,0 +1 @@ +fac6362a3cf2dcd191de4bd1eb44180fefe4d8a3 \ No newline at end of file diff --git a/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/pdfbox-parent-2.1.0-SNAPSHOT.pom b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/pdfbox-parent-2.1.0-SNAPSHOT.pom new file mode 100644 index 000000000..8f476cd3c --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/pdfbox-parent-2.1.0-SNAPSHOT.pom @@ -0,0 +1,378 @@ + + + + + + 4.0.0 + + + org.apache + apache + 16 + + + + org.apache.pdfbox + pdfbox-parent + 2.1.0-SNAPSHOT + pom + + PDFBox parent + 2002 + + + The Apache Software Foundation + http://pdfbox.apache.org + + + + jira + https://issues.apache.org/jira/browse/PDFBOX + + + + UTF-8 + UTF-8 + + + + 3.0.0 + + + + + + junit + junit + 4.12 + test + + + commons-logging + commons-logging + 1.2 + + + commons-io + commons-io + 2.4 + test + + + org.bouncycastle + bcprov-jdk15on + 1.55 + + + org.bouncycastle + bcmail-jdk15on + 1.55 + + + org.bouncycastle + bcpkix-jdk15on + 1.55 + + + log4j + log4j + 1.2.17 + + + + + com.levigo.jbig2 + levigo-jbig2-imageio + 1.6.5 + test + + + com.github.jai-imageio + jai-imageio-core + 1.3.1 + test + + + com.github.jai-imageio + jai-imageio-jpeg2000 + 1.3.0 + test + + + + + + + pedantic + + + + org.apache.rat + apache-rat-plugin + + + verify + + check + + + + + + + + + + + + + maven-compiler-plugin + + true + 1.6 + 1.6 + UTF-8 + + + + maven-javadoc-plugin + + 1.6 + + http://download.oracle.com/javase/1.6.0/docs/api/ + + UTF-8 + true + + + + maven-release-plugin + + false + deploy + -Papache-release,pedantic + true + + + + org.apache.maven.plugins + maven-jar-plugin + + + + true + true + + + + + + org.apache.maven.plugins + maven-source-plugin + + + attach-sources + + jar + + + + + + org.codehaus.mojo + animal-sniffer-maven-plugin + 1.14 + + + check-java-version + test + + check + + + + org.codehaus.mojo.signature + java16 + 1.0 + + + + + + + + + + org.apache.rat + apache-rat-plugin + 0.11 + + + release.properties + + + + + org.apache.felix + maven-bundle-plugin + + 2.5.4 + + + + + + + + + lehmi + Andreas Lehmkühler + + PMC Chair + + + + adam + Adam Nichols + + PMC Member + + + + blitchfield + Ben Litchfield + + PMC Member + + + + carrier + Brian Carrier + + PMC Member + + + + danielwilson + Daniel Wilson + + PMC Member + + + + gbailleul + Guillaume Bailleul + + PMC Member + + + + jeremias + Jeremias Maerki + + PMC Member + + + + koch + Johannes Koch + + PMC Member + + + + jahewson + John Hewson + + PMC Member + + + + kjackson + Kevin Jackson + + PMC Member + + + + msayhoun + Maruan Sayhoun + + PMC Member + + + + pkoch + Phillipp Koch + + PMC Member + + + + tallison + Tim Allison + + PMC Member + + + + tchojecki + Thomas Chojecki + + PMC Member + + + + tboehme + Timo Boehme + + PMC Member + + + + tilman + Tilman Hausherr + + PMC Member + + + + vfed + Villu Ruusmann + + PMC Member + + + + leleueri + Eric Leleu + + Emeritus PMC Member + + + + jukka + Jukka Zitting + + Emeritus PMC Member + + + + diff --git a/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/resolver-status.properties b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/resolver-status.properties new file mode 100644 index 000000000..7ad49eef7 --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox-parent/2.1.0-SNAPSHOT/resolver-status.properties @@ -0,0 +1,11 @@ +#NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice. +#Mon Nov 15 12:10:30 CST 2021 +maven-metadata-apache.snapshots.xml.error= +maven-metadata-.xml.error= +maven-metadata-clojars.xml.error= +maven-metadata-my.datomic.com.xml.error=Could not transfer metadata org.apache.pdfbox\:pdfbox-parent\:2.1.0-SNAPSHOT/maven-metadata.xml from/to my.datomic.com (https\://my.datomic.com/repo)\: Not authorized , ReasonPhrase\:Unauthorized. +maven-metadata-my.datomic.com.xml/@default-my.datomic.com-https\://my.datomic.com/repo/.lastUpdated=1636999829475 +maven-metadata-apache.xml.lastUpdated=1583301978767 +maven-metadata-clojars.xml.lastUpdated=1636999829358 +maven-metadata-.xml.lastUpdated=1636999829847 +maven-metadata-apache.snapshots.xml.lastUpdated=1636999830674 diff --git a/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/_remote.repositories b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/_remote.repositories new file mode 100644 index 000000000..ef06add2b --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/_remote.repositories @@ -0,0 +1,6 @@ +#NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice. +#Tue Jan 29 13:44:11 MST 2019 +pdfbox-2.1.0-20170324.170253-831.pom>apache= +pdfbox-2.1.0-20170324.170253-831.jar>apache= +pdfbox-2.1.0-20170324.170253-831.jar>= +pdfbox-2.1.0-20170324.170253-831.pom>= diff --git a/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-20170324.170253-831.jar b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-20170324.170253-831.jar new file mode 100644 index 000000000..e817c4e17 Binary files /dev/null and b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-20170324.170253-831.jar differ diff --git a/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-20170324.170253-831.jar.sha1 b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-20170324.170253-831.jar.sha1 new file mode 100644 index 000000000..60cb76b66 --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-20170324.170253-831.jar.sha1 @@ -0,0 +1 @@ +c79db0c550a8fd161065b810ef78fdbdcfaac457 \ No newline at end of file diff --git a/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-20170324.170253-831.pom b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-20170324.170253-831.pom new file mode 100644 index 000000000..d52ad3b5f --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-20170324.170253-831.pom @@ -0,0 +1,184 @@ + + + + + + 4.0.0 + + + org.apache.pdfbox + pdfbox-parent + 2.1.0-SNAPSHOT + ../parent/pom.xml + + + pdfbox + bundle + + Apache PDFBox + + The Apache PDFBox library is an open source Java tool for working with PDF documents. + + 2002 + + + git:https://git-wip-us.apache.org/repos/asf/pdfbox-docs//content/docs/${project.version}/javadocs + + + + + org.apache.pdfbox + fontbox + ${project.version} + + + commons-logging + commons-logging + + + org.bouncycastle + bcmail-jdk15on + true + + + org.bouncycastle + bcprov-jdk15on + true + + + junit + junit + test + + + com.googlecode.java-diff-utils + diffutils + 1.3.0 + test + + + + com.levigo.jbig2 + levigo-jbig2-imageio + test + + + com.github.jai-imageio + jai-imageio-core + test + + + com.github.jai-imageio + jai-imageio-jpeg2000 + test + + + + + + + src/main/resources + true + + **/*.properties + + + + src/main/resources + false + + **/*.properties + + + + + + maven-surefire-plugin + + -Xmx768m + + org/apache/pdfbox/rendering/TestPDFToImage.java + + + src/test/resources/logging.properties + + + + + org.apache.felix + maven-bundle-plugin + true + + + + {maven-resources}, + META-INF=target/maven-shared-archive-resources/META-INF, + org/apache/pdfbox/resources=target/classes/org/apache/pdfbox/resources + + + + + + org.apache.rat + apache-rat-plugin + + + src/main/resources/org/apache/pdfbox/resources/afm/* + src/main/resources/org/apache/pdfbox/resources/icc/* + src/main/resources/org/apache/pdfbox/resources/glyphlist/glyphlist.txt + src/main/resources/org/apache/pdfbox/resources/glyphlist/zapfdingbats.txt + src/main/resources/org/apache/pdfbox/resources/text/BidiMirroring.txt + src/main/resources/META-INF/services/* + src/test/resources/input/rendering/*.ai + src/test/resources/input/*.txt + src/test/resources/output/* + src/test/resources/org/apache/pdfbox/pdmodel/sRGB.* + release.properties + src/test/resources/org/apache/pdfbox/encryption/*.der + src/test/resources/org/apache/pdfbox/encryption/*.pfx + src/test/resources/org/apache/pdfbox/filter/*.bin + src/test/resources/org/apache/pdfbox/text/*.txt + src/main/java/org/apache/pdfbox/filter/CCITTFaxDecoderStream.java + src/main/java/org/apache/pdfbox/filter/CCITTFaxEncoderStream.java + src/main/java/org/apache/pdfbox/filter/TIFFExtension.java + + + + + + org.apache.maven.plugins + maven-scm-publish-plugin + + ${project.reporting.outputDirectory}/apidocs + scm:${git.scmJavadocUrl} + true + ${svn.scmJavadocCheckoutDirectory} + pdfbox-site + + + + + + + diff --git a/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-20170324.170253-831.pom.sha1 b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-20170324.170253-831.pom.sha1 new file mode 100644 index 000000000..727eee48e --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-20170324.170253-831.pom.sha1 @@ -0,0 +1 @@ +436c0adddfb7deaf65e5d4aab643dac33b6ce425 \ No newline at end of file diff --git a/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-SNAPSHOT.jar b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-SNAPSHOT.jar new file mode 100644 index 000000000..e817c4e17 Binary files /dev/null and b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-SNAPSHOT.jar differ diff --git a/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-SNAPSHOT.pom b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-SNAPSHOT.pom new file mode 100644 index 000000000..d52ad3b5f --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/pdfbox-2.1.0-SNAPSHOT.pom @@ -0,0 +1,184 @@ + + + + + + 4.0.0 + + + org.apache.pdfbox + pdfbox-parent + 2.1.0-SNAPSHOT + ../parent/pom.xml + + + pdfbox + bundle + + Apache PDFBox + + The Apache PDFBox library is an open source Java tool for working with PDF documents. + + 2002 + + + git:https://git-wip-us.apache.org/repos/asf/pdfbox-docs//content/docs/${project.version}/javadocs + + + + + org.apache.pdfbox + fontbox + ${project.version} + + + commons-logging + commons-logging + + + org.bouncycastle + bcmail-jdk15on + true + + + org.bouncycastle + bcprov-jdk15on + true + + + junit + junit + test + + + com.googlecode.java-diff-utils + diffutils + 1.3.0 + test + + + + com.levigo.jbig2 + levigo-jbig2-imageio + test + + + com.github.jai-imageio + jai-imageio-core + test + + + com.github.jai-imageio + jai-imageio-jpeg2000 + test + + + + + + + src/main/resources + true + + **/*.properties + + + + src/main/resources + false + + **/*.properties + + + + + + maven-surefire-plugin + + -Xmx768m + + org/apache/pdfbox/rendering/TestPDFToImage.java + + + src/test/resources/logging.properties + + + + + org.apache.felix + maven-bundle-plugin + true + + + + {maven-resources}, + META-INF=target/maven-shared-archive-resources/META-INF, + org/apache/pdfbox/resources=target/classes/org/apache/pdfbox/resources + + + + + + org.apache.rat + apache-rat-plugin + + + src/main/resources/org/apache/pdfbox/resources/afm/* + src/main/resources/org/apache/pdfbox/resources/icc/* + src/main/resources/org/apache/pdfbox/resources/glyphlist/glyphlist.txt + src/main/resources/org/apache/pdfbox/resources/glyphlist/zapfdingbats.txt + src/main/resources/org/apache/pdfbox/resources/text/BidiMirroring.txt + src/main/resources/META-INF/services/* + src/test/resources/input/rendering/*.ai + src/test/resources/input/*.txt + src/test/resources/output/* + src/test/resources/org/apache/pdfbox/pdmodel/sRGB.* + release.properties + src/test/resources/org/apache/pdfbox/encryption/*.der + src/test/resources/org/apache/pdfbox/encryption/*.pfx + src/test/resources/org/apache/pdfbox/filter/*.bin + src/test/resources/org/apache/pdfbox/text/*.txt + src/main/java/org/apache/pdfbox/filter/CCITTFaxDecoderStream.java + src/main/java/org/apache/pdfbox/filter/CCITTFaxEncoderStream.java + src/main/java/org/apache/pdfbox/filter/TIFFExtension.java + + + + + + org.apache.maven.plugins + maven-scm-publish-plugin + + ${project.reporting.outputDirectory}/apidocs + scm:${git.scmJavadocUrl} + true + ${svn.scmJavadocCheckoutDirectory} + pdfbox-site + + + + + + + diff --git a/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/resolver-status.properties b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/resolver-status.properties new file mode 100644 index 000000000..6b5d92bcc --- /dev/null +++ b/lib/org/apache/pdfbox/pdfbox/2.1.0-SNAPSHOT/resolver-status.properties @@ -0,0 +1,8 @@ +#NOTE: This is a Maven Resolver internal implementation file, its format can be changed without prior notice. +#Mon Nov 15 12:10:29 CST 2021 +maven-metadata-.xml.error= +maven-metadata-my.datomic.com.xml.error=Could not transfer metadata org.apache.pdfbox\:pdfbox\:2.1.0-SNAPSHOT/maven-metadata.xml from/to my.datomic.com (https\://my.datomic.com/repo)\: Not authorized , ReasonPhrase\:Unauthorized. +maven-metadata-my.datomic.com.xml/@default-my.datomic.com-https\://my.datomic.com/repo/.lastUpdated=1636999828939 +maven-metadata-clojars.xml.error= +maven-metadata-.xml.lastUpdated=1636999829305 +maven-metadata-clojars.xml.lastUpdated=1636999828874 diff --git a/main.js b/main.js deleted file mode 100644 index 5635ff4d8..000000000 --- a/main.js +++ /dev/null @@ -1,8 +0,0 @@ -'use strict'; - -// cljsbuild adds a preamble mentioning goog so hack around it -window.goog = { - provide() {}, - require() {}, -}; -require('./target/env/index.js'); diff --git a/native/cljs/orcpub/core.cljs b/native/cljs/orcpub/core.cljs deleted file mode 100644 index bfabafe49..000000000 --- a/native/cljs/orcpub/core.cljs +++ /dev/null @@ -1,32 +0,0 @@ -(ns orcpub.core - (:require [reagent.core :as r :refer [atom]] - [re-frame.core :refer [subscribe dispatch dispatch-sync]] - [orcpub.dnd.e5.events] - [orcpub.dnd.e5.equipment-subs] - [orcpub.dnd.e5.subs] - [orcpub.dnd.e5.db] - [orcpub.views :refer [text view image touchable-without-feedback app-registry Alert]] - [orcpub.dnd.e5.native-views :as v5e])) - -(defn alert [title] - (.alert Alert title)) - -(defn hello-button [] - [touchable-without-feedback {:on-press #(alert "HELLO!")} - [view {:style {:border-width 1 :border-color "#f0a100" :padding 10 :border-radius 5}} - [text {:style {:color "white" :text-align "center" :font-weight "bold"}} "press me"]]]) - -(defn app-root [] - (prn "APP ROOT") - [view {:style {:flex 1}} - [view {:style {:background-color "#313a4d" - :padding-top 20 - :padding-bottom 5 - :padding-left 5 - :padding-right 5}} - [image {:source (js/require "./assets/images/orcpub-logo.png")}]] - [v5e/character-builder]]) - -(defn init [] - (dispatch-sync [:initialize-db]) - (.registerComponent app-registry "main" #(r/reactify-component app-root))) diff --git a/native/cljs/orcpub/dnd/e5/native-views.cljs b/native/cljs/orcpub/dnd/e5/native-views.cljs deleted file mode 100644 index cb4f7a2fb..000000000 --- a/native/cljs/orcpub/dnd/e5/native-views.cljs +++ /dev/null @@ -1,324 +0,0 @@ -(ns orcpub.dnd.e5.native-views - (:require [orcpub.views :refer [view - scroll-view - text - touchable-without-feedback - main-text-color - light-text-color]] - [orcpub.entity :as entity] - [orcpub.views-aux :as views-aux] - [orcpub.template :as t] - [clojure.string :as s] - [reagent.core :as r] - [clojure.pprint :refer [pprint]] - [re-frame.core :refer [subscribe dispatch dispatch-sync]])) - -(def tab-style - {:padding 10 - :flex 1 - :border-bottom-color main-text-color - :opacity 0.4 - :height 45 - :flex-direction :row - :justify-content :space-around}) - -(def builder-tab-style - (merge - tab-style - {:border-bottom-width 5})) - -(def selected-tab-style - {:opacity 1}) - -(def selected-builder-tab-style - (merge - builder-tab-style - selected-tab-style)) - -(defn builder-tab [title key selected-tab] - [touchable-without-feedback {:on-press #(reset! selected-tab key)} - [view {:style (if (= key @selected-tab) - selected-builder-tab-style - builder-tab-style)} - [text {:style {:font-weight :bold - :color main-text-color - :font-size 18}} - title]]]) - -(def pages - [{:name "Race" - :icon "woman-elf-face" - :tags #{:race :subrace}} - {:name "Background" - :icon "ages" - :tags #{:background}} - {:name "Proficiencies" - :icon "juggler" - :tags #{:profs}}]) - -(def options-tab-style - (merge - tab-style - {:border-bottom-width 2 - :height 40})) - -(def selected-options-tab-style - (merge - options-tab-style - selected-tab-style)) - -(defn options-tab [title i selected-tab] - [touchable-without-feedback {:on-press #(do (prn "I" i) (reset! selected-tab i))} - [view {:style (if (= i @selected-tab) - selected-options-tab-style - options-tab-style)} - [text {:style {:font-weight :bold - :color main-text-color - :font-size 12}} - title]]]) - -(def unselected-option-style - {:padding 12 - :margin 2 - :border-width 2 - :border-radius 5 - :border-color light-text-color}) - -(def selected-option-style - (assoc - unselected-option-style - :border-width 5 - :border-color main-text-color)) - -(defn option-view [option-path - selection - disable-select-new? - homebrew? - option] - (let [{:keys [name - key - selected? - selectable? - multiselect? - option-path - select-fn - help - has-named-mods? - modifiers-str - failed-prereqs] :as data} - (views-aux/option-selector-data option-path - selection - disable-select-new? - homebrew? - option)] - [touchable-without-feedback - {:on-press select-fn} - [view {:style (if selected? - selected-option-style - unselected-option-style)} - [text name]]])) - -(defn selection-section-title [title] - (prn "SELETCION SECTION TITLE" title) - [view {:style {:margin-left 5}} - [text {:style {:font-size 16 - :font-weight :bold}} - title]]) - -(defn selection-section-parent-title [title] - (prn "SELECTION SECTION PARENT TITLE" title) - [view {:style {:margin-left 5 - :margin-bottom 2}} - [text {:style {:font-style :italic - :font-size 14 - :color light-text-color}} - title]]) - -(defn align-items-c [s] - (assoc s :align-items :center)) - -(defn h [s v] - (assoc s :height v)) - -(defn w [s v] - (assoc s :width v)) - -(defn i [s] - (assoc s :font-style :italic)) - -(defn remaining-bubble [value color left-offset top-offset] - [view {:style {:background-color color - :border-color color - :border-radius 12 - :border-width 12}} - [text - {:style {:position :absolute - :left left-offset - :top top-offset - :font-weight :bold - :font-size (or font-size 14) - :color :white}} - value]]) - -(defn remaining-indicator [remaining & [size font-size]] - (remaining-bubble remaining :red -4 -8)) - -(def remaining-text-style - {:margin-left 5 - :font-style :italic}) - -(def remaining-view-style - {:align-items :center - :flex-direction :row}) - -(defn remaining-component [max remaining] - [view {:style {:margin-left 10}} - (cond - (pos? remaining) - [view {:style remaining-view-style} - (remaining-indicator remaining) - [text {:style remaining-text-style} - "remaining"]] - - (or (zero? remaining) - (and (nil? max) - (neg? remaining))) - [view {:style remaining-view-style} - (remaining-bubble "\u2713" :green -6 -9) - [text {:style remaining-text-style} - "complete"]] - - (neg? remaining) - [view {:style remaining-view-style} - [text {:style {:font-style :italic - :margin-right 5}} - "remove"] - (remaining-bubble (Math/abs remaining) :red -4 -8)])]) - -(defn selection-section-base [] - (let [expanded? (r/atom false)] - (fn [{:keys [title path parent-title name icon help max min remaining body hide-lock? hide-homebrew?]}] - (let [locked? @(subscribe [:locked path]) - homebrew? @(subscribe [:homebrew? path])] - [view {:style {:padding 5 - :margin-bottom 20}} - (if (and (or title name) parent-title) - (selection-section-parent-title parent-title)) - [view - #_(if icon (views5e/svg-icon icon 24)) - (if (or title name) - (selection-section-title (or title name)) - (if parent-title - (selection-section-parent-title parent-title))) - #_(if (and path help) - [show-info-button expanded?]) - #_(if (not hide-lock?) - [:i.fa.f-s-16.m-l-10.m-r-5.pointer - {:class-name (if locked? "fa-lock" "fa-unlock-alt opacity-5 hover-opacity-full") - :on-click #(dispatch [:toggle-locked path])}]) - #_(if (not hide-homebrew?) - [:span.pointer - {:class-name (if (not homebrew?) "opacity-5 hover-opacity-full") - :on-click #(dispatch [:toggle-homebrew path])} - (views5e/svg-icon "beer-stein" 18)])] - #_(if (and help path @expanded?) - [help-section help]) - (if (int? min) - [view {:style {:flex-direction :row - :align-items :center - :padding-horizontal 5 - :justify-content :space-between}} - [text {:style {:font-style :italic}} - (str "select " (cond - (= min max) min - (zero? min) (if (nil? max) - "any number" - (str "up to " max)) - :else (str "at least " min)))] - (remaining-component max remaining)]) - body])))) - -(defn selection-view [path {:keys [::t/key ::t/name ::t/options] :as selection} _ _ _] - [view {:style {:margin-bottom 10}} - (doall - (map - (fn [{:keys [::t/key ::t/name] :as option}] - ^{:key key} - [option-view - path - selection - false - false - option]) - options))]) - -(defn selection-section [title built-template option-paths ui-fns selection num-columns remaining & [hide-homebrew?]] - (let [path (entity/actual-path selection) - {:keys [disable-select-new? homebrew?] :as data} - (views-aux/selection-section-data - title - built-template - option-paths - ui-fns - selection-view - selection - num-columns - remaining - hide-homebrew?)] - [selection-section-base data])) - -(defn build-view [] - (let [selected-tab-index (r/atom 0)] - (fn [] - (let [character @(subscribe [:character]) - built-template @(subscribe [:built-template]) - available-selections @(subscribe [:available-selections]) - option-paths @(subscribe [:option-paths]) - built-char @(subscribe [:built-character]) - {:keys [tags ui-fns components] :as page} (pages @selected-tab-index) - selections (entity/tagged-selections available-selections tags) - final-selections (entity/combine-selections selections)] - (pprint character) - [view {:style {:flex 1}} - [view {:style {:flex-direction :row}} - [view {:style {:flex-direction :row - :flex 1 - :padding 10 - :justify-content :space-around}} - (doall - (map-indexed - (fn [i {:keys [name icon tags]}] - ^{:key name} - [options-tab name i selected-tab-index]) - pages))]] - [scroll-view {:style {:padding 10}} - (doall - (map - (fn [{:keys [::t/key ::t/name] :as selection}] - (let [path (entity/actual-path selection)] - ^{:key (s/join "," path)} - [selection-section - name - built-template - option-paths - ui-fns - selection - 1 - (entity/count-remaining built-template character selection) - false])) - final-selections))]])))) - - -(defn character-builder [] - (let [selected-tab (r/atom :options)] - [view {:style {:align-items :center - :flex 1}} - [view {:style {:flex-direction :row - :margin-top 10 - :padding 10}} - [builder-tab "BUILD" :options selected-tab] - [builder-tab "DESCRIBE" :description selected-tab] - [builder-tab "VIEW" :sheet selected-tab]] - [view {:style {:flex 1 - :flex-direction :row}} - [build-view]]])) diff --git a/native/cljs/orcpub/views.cljs b/native/cljs/orcpub/views.cljs deleted file mode 100644 index 7dab2a6b3..000000000 --- a/native/cljs/orcpub/views.cljs +++ /dev/null @@ -1,22 +0,0 @@ -(ns orcpub.views - (:require [reagent.core :as r :refer [atom]])) - -(def ReactNative (js/require "react-native")) -#_(def FontAwesome (js/require "react-native-fontawesome")) -#_(def FontAwesomeIcons (.-Icons FontAwesome)) - -#_(def fa (r/adapt-react-class FontAwesome)) -#_(defn fa-icon [icon-name] - [fa (aget FontAwesomeIcons icon-name)]) - -(def app-registry (.-AppRegistry ReactNative)) -(def text (r/adapt-react-class (.-Text ReactNative))) -(def view (r/adapt-react-class (.-View ReactNative))) -(def scroll-view (r/adapt-react-class (.-ScrollView ReactNative))) -(def image (r/adapt-react-class (.-Image ReactNative))) -(def touchable-highlight (r/adapt-react-class (.-TouchableHighlight ReactNative))) -(def touchable-without-feedback (r/adapt-react-class (.-TouchableWithoutFeedback ReactNative))) -(def Alert (.-Alert ReactNative)) - -(def main-text-color "#727272") -(def light-text-color "#BBBBBB") diff --git a/package.json b/package.json deleted file mode 100644 index 5b30b93c2..000000000 --- a/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "orcpub", - "version": "0.0.1", - "description": "", - "author": "", - "private": true, - "main": "main.js", - "dependencies": { - "expo": "^17.0.0", - "react": "16.0.0-alpha.6", - "react-native": "https://github.com/expo/react-native/archive/sdk-17.0.0.tar.gz", - "react-native-fontawesome": "^5.7.0" - } -} diff --git a/project.clj b/project.clj index 02acd6b21..c04aeb7ba 100644 --- a/project.clj +++ b/project.clj @@ -1,3 +1,8 @@ +; Allow http connection, as org.apache.pdfbox/pdfbox has http dependnecies +(require 'cemerick.pomegranate.aether) +(cemerick.pomegranate.aether/register-wagon-factory! + "http" #(org.apache.maven.wagon.providers.http.HttpWagon.)) + (defproject orcpub "0.1.0-SNAPSHOT" :description "FIXME: write this!" :url "http://example.com/FIXME" @@ -10,58 +15,67 @@ :repositories [["apache" "http://repository.apache.org/snapshots/"] ["my.datomic.com" {:url "https://my.datomic.com/repo" :username [:gpg :env] - :password [:gpg :env]}]] - - :dependencies [[org.clojure/clojure "1.9.0-RC1"] + :password [:gpg :env]}] + ; This allows us to seamlessly load jars from local disk. + ["local" {:url "file:lib" + :checksum :ignore + :releases {:checksum :ignore}}] + ] + :mirrors {"apache" {:url "https://repository.apache.org/snapshots/"}} + + :dependencies [[org.clojure/clojure "1.10.0"] [org.clojure/test.check "0.9.0"] - [org.clojure/clojurescript "1.9.946"] - [org.clojure/core.async "0.3.443"] - [cljsjs/react "15.3.1-0"] - [cljsjs/react-dom "15.3.1-0"] - [cljsjs/facebook "v20150729-0"] - [cljsjs/google-platformjs-extern "1.0.0-0"] + [org.clojure/clojurescript "1.10.439"] + [org.clojure/core.async "0.4.490"] + [cljsjs/react "16.6.0-0"] + [cljsjs/react-dom "16.6.0-0"] [cljsjs/filesaverjs "1.3.3-0"] - [com.cognitect/transit-cljs "0.8.243"] - [cljs-http "0.1.44"] - [com.andrewmcveigh/cljs-time "0.5.0"] - [clj-time "0.14.0"] - [clj-http "3.6.1"] + [com.cognitect/transit-cljs "0.8.256"] + [cljs-http "0.1.45"] + [com.andrewmcveigh/cljs-time "0.5.2"] + [clj-time "0.15.0"] + [clj-http "3.9.1"] [com.yetanalytics/ring-etag-middleware "0.1.1"] [org.clojure/test.check "0.9.0"] - + [org.clojure/core.match "0.3.0-alpha5"] - [re-frame "0.9.0"] + [re-frame "0.10.9"] [reagent "0.7.0"] [garden "1.3.2"] - [org.apache.pdfbox/pdfbox "2.1.0-20170324.170253-831"] - [io.pedestal/pedestal.service "0.5.1"] + [org.apache.pdfbox/pdfbox "2.1.0-SNAPSHOT"] + [io.pedestal/pedestal.service "0.5.1"] [io.pedestal/pedestal.route "0.5.1"] [io.pedestal/pedestal.jetty "0.5.1"] - [org.clojure/data.json "0.2.6"] - [org.slf4j/slf4j-simple "1.7.21"] + [org.clojure/data.json "0.2.6"] + [org.slf4j/slf4j-simple "1.7.21"] [buddy/buddy-auth "1.4.1"] - [buddy/buddy-hashers "1.2.0"] + [buddy/buddy-hashers "1.2.0"] [reloaded.repl "0.2.3"] [bidi "2.0.17"] [com.stuartsierra/component "0.3.2"] [com.google.guava/guava "21.0"] - [com.amazonaws/aws-java-sdk-dynamodb "1.11.6"] - [com.fasterxml.jackson.core/jackson-databind "2.7.0"] + [com.fasterxml.jackson.core/jackson-databind "2.11.1"] - [binaryage/devtools "0.9.4"] [hiccup "1.0.5"] [com.draines/postal "2.0.2"] [environ "1.1.0"] - [pdfkit-clj "0.1.6"] - [vvvvalvalval/datomock "0.2.0"]] + [pdfkit-clj "0.1.7"] + [vvvvalvalval/datomock "0.2.0"] + [com.datomic/datomic-free "0.9.5697"] + [funcool/cuerdas "2.2.0"] + [camel-snake-kebab "0.4.0"] + [org.webjars/font-awesome "5.13.1"]] - :plugins [[lein-figwheel "0.5.14"] - [lein-cljsbuild "1.1.6" :exclusions [[org.clojure/clojure]]] + :plugins [[lein-figwheel "0.5.19"] + [lein-cljsbuild "1.1.7" :exclusions [[org.clojure/clojure]]] + [lein-localrepo "0.5.4"] [lein-garden "0.3.0"] [lein-environ "1.1.0"] + [lein-cljfmt "0.6.8"] + [lein-kibit "0.1.8"] #_[lein-resource "16.9.1"]] :source-paths ["src/clj" "src/cljc" "src/cljs"] @@ -70,55 +84,49 @@ :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] - :resource-paths ["resources" "resources/.ebextensions/*.config"] + :resource-paths ["resources" "resources/.ebextensions/"] :uberjar-name "orcpub.jar" - :garden {:builds [{ ;; Optional name of the build: + :garden {:builds [{;; Optional name of the build: :id "screen" ;; Source paths where the stylesheet source code is :source-paths ["src/clj" "src/cljc"] ;; The var containing your stylesheet: :stylesheet orcpub.styles.core/app ;; Compiler flags passed to `garden.core/css`: - :compiler { ;; Where to save the file: + :compiler {;; Where to save the file: :output-to "resources/public/css/compiled/styles.css" ;; Compress the output? :pretty-print? false}}]} + :prep-tasks [["garden" "once"]] + :cljsbuild {:builds - [{:id "dev" - :source-paths ["web/cljs" "src/cljc" "src/cljs"] + {:dev + {:source-paths ["web/cljs" "src/cljc" "src/cljs"] ;; the presence of a :figwheel configuration here ;; will cause figwheel to inject the figwheel client ;; into your build - :figwheel {:on-jsload "orcpub.core/on-js-reload" - ;; :open-urls will pop open your application - ;; in the default browser once Figwheel has - ;; started and complied your application. - ;; Comment this out once it no longer serves you. - :open-urls ["http://localhost:3449/index.html"]} - - :compiler {:main orcpub.core - :asset-path "/js/compiled/out" - :output-to "resources/public/js/compiled/orcpub.js" - :output-dir "resources/public/js/compiled/out" - :source-map-timestamp true - ;; To console.log CLJS data-structures make sure you enable devtools in Chrome - ;; https://github.com/binaryage/cljs-devtools - :preloads [devtools.preload]}} - ;; This next build is an compressed minified build for - ;; production. You can build this with: - ;; lein cljsbuild once min - #_{:id "min" - :source-paths ["src/cljc" "src/cljs"] - :compiler {:output-to "resources/public/js/compiled/orcpub.js" - :main orcpub.core - :optimizations :advanced - :pretty-print false}}]} - - :figwheel { ;; :http-server-root "public" ;; default and assumes "resources" + :figwheel {:on-jsload "orcpub.core/on-js-reload" + ;; :open-urls will pop open your application + ;; in the default browser once Figwheel has + ;; started and complied your application. + ;; Comment this out once it no longer serves you. + :open-urls ["http://localhost:8890"]} + + :compiler {:main orcpub.core + :asset-path "/js/compiled/out" + :output-to "resources/public/js/compiled/orcpub.js" + :output-dir "resources/public/js/compiled/out" + :source-map-timestamp true + :pretty-print true + :closure-defines {goog.DEBUG true} + :optimizations :none + }}}} + + :figwheel {;; :http-server-root "public" ;; default and assumes "resources" ;; :server-port 3449 ;; default ;; :server-ip "127.0.0.1" @@ -154,71 +162,87 @@ ;; :server-logfile "tmp/logs/figwheel-logfile.log" } + :repl-options {;; If nREPL takes too long to load it may timeout, + ;; increase this to wait longer before timing out. + ;; Defaults to 30000 (30 seconds) + :timeout 300000 ; 5 mins to wait + } ;; setting up nREPL for Figwheel and ClojureScript dev ;; Please see: ;; https://github.com/bhauman/lein-figwheel/wiki/Using-the-Figwheel-REPL-within-NRepl - :uberjar-inclusions [".ebextensions"] - :jar-inclusions [".ebextensions"] + :uberjar-inclusions [#"^\.ebextensions"] + :jar-inclusions [#"^\.ebextensions"] :aliases {"figwheel-native" ["with-profile" "native-dev" "run" "-m" "user" "--figwheel"] ;;"figwheel-web" ["figwheel"] "externs" ["do" "clean" ["run" "-m" "externs"]] "rebuild-modules" ["run" "-m" "user" "--rebuild-modules"] + "lint" ["with-profile" "lint" "run" "-m" "clj-kondo.main" "--lint" "src"] "prod-build" ^{:doc "Recompile code with prod profile."} ["externs" ["with-profile" "prod" "cljsbuild" "once" "main"]]} - :profiles {:dev {:dependencies [[com.datomic/datomic-free "0.9.5561"] - - [binaryage/devtools "0.9.4"] - [figwheel-sidecar "0.5.14"] - [com.cemerick/piggieback "0.2.1"] - [org.clojure/test.check "0.9.0"]] - ;; need to add dev source path here to get user.clj loaded - :source-paths ["src/clj" "src/cljc" "src/cljs" "dev"] - ;; for CIDER - ;; :plugins [[cider/cider-nrepl "0.12.0"]] - :repl-options { ; for nREPL dev you really need to limit output - :init (set! *print-length* 50) - :init-ns user - :nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}} - :native-dev {:dependencies [[figwheel-sidecar "0.5.14"] - [com.cemerick/piggieback "0.2.1"] - [org.clojure/test.check "0.9.0"]] - :source-paths ["src/cljs" "native/cljs" "src/cljc" "env/dev"] - :cljsbuild {:builds [{:id "main" - :source-paths ["src/cljs" "native/cljs" "src/cljc" "env/dev"] - :figwheel true - :compiler {:output-to "target/not-used.js" - :main "env.main" - :output-dir "target" - :optimizations :none}}]} - :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}} - :prod {:cljsbuild {:builds [{:id "main" - :source-paths ["src/cljs" "native/cljs" "src/cljc" "env/prod"] - :compiler {:output-to "main.js" - :main "env.main" - :output-dir "target" - :static-fns true - :externs ["js/externs.js"] - :parallel-build true - :optimize-constants true - :optimizations :advanced - :closure-defines {"goog.DEBUG" false}}}]} - :dependencies [[com.datomic/datomic-pro "0.9.5561"]]} - :uberjar {:prep-tasks ["clean" "compile" ["cljsbuild" "once" "prod"]] - :env {:production true} - :aot :all - :omit-source true - :cljsbuild {:builds - [{:id "prod" - :source-paths ["web/cljs" "src/cljc" "src/cljs"] - :compiler {:main orcpub.core - :asset-path "/js/compiled/out" - :output-to "resources/public/js/compiled/orcpub.js" - ;;:output-dir "resources/public/js/compiled/out" - :optimizations :advanced - :pretty-print false - }}]}}}) + :profiles {:dev {:dependencies [[binaryage/devtools "0.9.10"] + [figwheel-sidecar "0.5.19"] + [cider/piggieback "0.4.0"] + [org.clojure/test.check "0.9.0"] + [day8.re-frame/re-frame-10x "0.3.7"]] + :env {:dev-mode "true"} + ;; need to add dev source path here to get user.clj loaded + :source-paths ["web/cljs" "src/clj" "src/cljc" "src/cljs" "dev"] + :cljsbuild {:builds {:dev {:compiler {:closure-defines {"re_frame.trace.trace_enabled_QMARK_" true + goog.DEBUG true + } + :optimizations :none + :pretty-print true + ;; To console.log CLJS data-structures make sure you enable devtools in Chrome + ;; https://github.com/binaryage/cljs-devtools + :preloads [devtools.preload day8.re-frame-10x.preload]}}}} + ;; for CIDER + ;; :plugins [[cider/cider-nrepl "0.12.0"]] + :repl-options {:init-ns user + :nrepl-middleware [cider.piggieback/wrap-cljs-repl]}} + :native-dev {:dependencies [[figwheel-sidecar "0.5.19"] + [com.cemerick/piggieback "0.2.1"] + [org.clojure/test.check "0.9.0"]] + :source-paths ["src/cljs" "native/cljs" "src/cljc" "env/dev"] + :cljsbuild {:builds [{:id "main" + :source-paths ["src/cljs" "native/cljs" "src/cljc" "env/dev"] + :figwheel true + :compiler {:output-to "target/not-used.js" + :main "env.main" + :output-dir "target" + :optimizations :none}}]} + :repl-options {:nrepl-middleware [cemerick.piggieback/wrap-cljs-repl]}} + :prod {:cljsbuild {:builds [{:id "main" + :source-paths ["src/cljs" "native/cljs" "src/cljc" "env/prod"] + :compiler {:output-to "main.js" + :main "env.main" + :output-dir "target" + :static-fns true + :externs ["js/externs.js"] + :parallel-build true + :optimize-constants true + :optimizations :advanced}}]} + :dependencies [[com.datomic/datomic-free "0.9.5697"]]} + :uberjar {:prep-tasks ["clean" "compile" ["cljsbuild" "once" "prod"]] + :env {:production true} + :aot :all + :omit-source true + :cljsbuild {:builds + {:prod + {:source-paths ["web/cljs" "src/cljc" "src/cljs"] + :compiler {:main orcpub.core + :asset-path "/js/compiled/out" + :output-to "resources/public/js/compiled/orcpub.js" + ;;:output-dir "resources/public/js/compiled/out" + :optimizations :advanced + :pretty-print false}}}}} + :lint {:dependencies [[clj-kondo "2024.05.22"]] + :clj-kondo {:linters {:shadowed-fn-param {:level :off} + :shadowed-var {:level :off}}}} + ;; Use like: lein with-profile +start-server repl + :start-server {:repl-options {:init-ns user + :init (start-server)}}}) diff --git a/resources/.ebextensions/https-instance-securitygroup.config b/resources/.ebextensions/https-instance-securitygroup.config deleted file mode 100644 index 578ee1f92..000000000 --- a/resources/.ebextensions/https-instance-securitygroup.config +++ /dev/null @@ -1,9 +0,0 @@ -Resources: - sslSecurityGroupIngress: - Type: AWS::EC2::SecurityGroupIngress - Properties: - GroupId: {"Fn::GetAtt" : ["AWSEBSecurityGroup", "GroupId"]} - IpProtocol: tcp - ToPort: 443 - FromPort: 443 - CidrIp: 0.0.0.0/0 \ No newline at end of file diff --git a/resources/copy/.ebextensions/test.config b/resources/copy/.ebextensions/test.config deleted file mode 100644 index e69de29bb..000000000 diff --git a/resources/fillable-char-sheet-0-spells.pdf b/resources/fillable-char-sheet-0-spells.pdf deleted file mode 100644 index 817820679..000000000 Binary files a/resources/fillable-char-sheet-0-spells.pdf and /dev/null differ diff --git a/resources/fillable-char-sheet-1-spells.pdf b/resources/fillable-char-sheet-1-spells.pdf deleted file mode 100644 index 319a1d9b8..000000000 Binary files a/resources/fillable-char-sheet-1-spells.pdf and /dev/null differ diff --git a/resources/fillable-char-sheet-2-spells.pdf b/resources/fillable-char-sheet-2-spells.pdf deleted file mode 100644 index 1ddde8906..000000000 Binary files a/resources/fillable-char-sheet-2-spells.pdf and /dev/null differ diff --git a/resources/fillable-char-sheet-3-spells.pdf b/resources/fillable-char-sheet-3-spells.pdf deleted file mode 100644 index 1babeccc5..000000000 Binary files a/resources/fillable-char-sheet-3-spells.pdf and /dev/null differ diff --git a/resources/fillable-char-sheet-4-spells.pdf b/resources/fillable-char-sheet-4-spells.pdf deleted file mode 100644 index d0aae82b4..000000000 Binary files a/resources/fillable-char-sheet-4-spells.pdf and /dev/null differ diff --git a/resources/fillable-char-sheet-5-spells.pdf b/resources/fillable-char-sheet-5-spells.pdf deleted file mode 100644 index 32ed35cfa..000000000 Binary files a/resources/fillable-char-sheet-5-spells.pdf and /dev/null differ diff --git a/resources/fillable-char-sheet-6-spells.pdf b/resources/fillable-char-sheet-6-spells.pdf deleted file mode 100644 index 7c36dc243..000000000 Binary files a/resources/fillable-char-sheet-6-spells.pdf and /dev/null differ diff --git a/resources/fillable-char-sheetstyle-1-0-spells.pdf b/resources/fillable-char-sheetstyle-1-0-spells.pdf new file mode 100644 index 000000000..b42e2e644 Binary files /dev/null and b/resources/fillable-char-sheetstyle-1-0-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-1-1-spells.pdf b/resources/fillable-char-sheetstyle-1-1-spells.pdf new file mode 100644 index 000000000..70beb892f Binary files /dev/null and b/resources/fillable-char-sheetstyle-1-1-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-1-2-spells.pdf b/resources/fillable-char-sheetstyle-1-2-spells.pdf new file mode 100644 index 000000000..58992cdc4 Binary files /dev/null and b/resources/fillable-char-sheetstyle-1-2-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-1-3-spells.pdf b/resources/fillable-char-sheetstyle-1-3-spells.pdf new file mode 100644 index 000000000..203629d2b Binary files /dev/null and b/resources/fillable-char-sheetstyle-1-3-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-1-4-spells.pdf b/resources/fillable-char-sheetstyle-1-4-spells.pdf new file mode 100644 index 000000000..1dfb3d7b8 Binary files /dev/null and b/resources/fillable-char-sheetstyle-1-4-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-1-5-spells.pdf b/resources/fillable-char-sheetstyle-1-5-spells.pdf new file mode 100644 index 000000000..cb66229f8 Binary files /dev/null and b/resources/fillable-char-sheetstyle-1-5-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-1-6-spells.pdf b/resources/fillable-char-sheetstyle-1-6-spells.pdf new file mode 100644 index 000000000..d2c99b86e Binary files /dev/null and b/resources/fillable-char-sheetstyle-1-6-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-2-0-spells.pdf b/resources/fillable-char-sheetstyle-2-0-spells.pdf new file mode 100644 index 000000000..c546c901d Binary files /dev/null and b/resources/fillable-char-sheetstyle-2-0-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-2-1-spells.pdf b/resources/fillable-char-sheetstyle-2-1-spells.pdf new file mode 100644 index 000000000..0a5426e5c Binary files /dev/null and b/resources/fillable-char-sheetstyle-2-1-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-2-2-spells.pdf b/resources/fillable-char-sheetstyle-2-2-spells.pdf new file mode 100644 index 000000000..2e8e2e0d2 Binary files /dev/null and b/resources/fillable-char-sheetstyle-2-2-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-2-3-spells.pdf b/resources/fillable-char-sheetstyle-2-3-spells.pdf new file mode 100644 index 000000000..b35fae1d7 Binary files /dev/null and b/resources/fillable-char-sheetstyle-2-3-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-2-4-spells.pdf b/resources/fillable-char-sheetstyle-2-4-spells.pdf new file mode 100644 index 000000000..cb6ac4934 Binary files /dev/null and b/resources/fillable-char-sheetstyle-2-4-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-2-5-spells.pdf b/resources/fillable-char-sheetstyle-2-5-spells.pdf new file mode 100644 index 000000000..67a02c9db Binary files /dev/null and b/resources/fillable-char-sheetstyle-2-5-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-2-6-spells.pdf b/resources/fillable-char-sheetstyle-2-6-spells.pdf new file mode 100644 index 000000000..22a3a8207 Binary files /dev/null and b/resources/fillable-char-sheetstyle-2-6-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-3-0-spells.pdf b/resources/fillable-char-sheetstyle-3-0-spells.pdf new file mode 100644 index 000000000..385f22767 Binary files /dev/null and b/resources/fillable-char-sheetstyle-3-0-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-3-1-spells.pdf b/resources/fillable-char-sheetstyle-3-1-spells.pdf new file mode 100644 index 000000000..a45146980 Binary files /dev/null and b/resources/fillable-char-sheetstyle-3-1-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-3-2-spells.pdf b/resources/fillable-char-sheetstyle-3-2-spells.pdf new file mode 100644 index 000000000..01930b69c Binary files /dev/null and b/resources/fillable-char-sheetstyle-3-2-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-3-3-spells.pdf b/resources/fillable-char-sheetstyle-3-3-spells.pdf new file mode 100644 index 000000000..1b5a20768 Binary files /dev/null and b/resources/fillable-char-sheetstyle-3-3-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-3-4-spells.pdf b/resources/fillable-char-sheetstyle-3-4-spells.pdf new file mode 100644 index 000000000..25d37ac38 Binary files /dev/null and b/resources/fillable-char-sheetstyle-3-4-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-3-5-spells.pdf b/resources/fillable-char-sheetstyle-3-5-spells.pdf new file mode 100644 index 000000000..2d2ff06b9 Binary files /dev/null and b/resources/fillable-char-sheetstyle-3-5-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-3-6-spells.pdf b/resources/fillable-char-sheetstyle-3-6-spells.pdf new file mode 100644 index 000000000..657eda0ba Binary files /dev/null and b/resources/fillable-char-sheetstyle-3-6-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-4-0-spells.pdf b/resources/fillable-char-sheetstyle-4-0-spells.pdf new file mode 100644 index 000000000..14d4de431 Binary files /dev/null and b/resources/fillable-char-sheetstyle-4-0-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-4-1-spells.pdf b/resources/fillable-char-sheetstyle-4-1-spells.pdf new file mode 100644 index 000000000..6d4d6266f Binary files /dev/null and b/resources/fillable-char-sheetstyle-4-1-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-4-2-spells.pdf b/resources/fillable-char-sheetstyle-4-2-spells.pdf new file mode 100644 index 000000000..483a138cb Binary files /dev/null and b/resources/fillable-char-sheetstyle-4-2-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-4-3-spells.pdf b/resources/fillable-char-sheetstyle-4-3-spells.pdf new file mode 100644 index 000000000..47d80a021 Binary files /dev/null and b/resources/fillable-char-sheetstyle-4-3-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-4-4-spells.pdf b/resources/fillable-char-sheetstyle-4-4-spells.pdf new file mode 100644 index 000000000..0a75558fd Binary files /dev/null and b/resources/fillable-char-sheetstyle-4-4-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-4-5-spells.pdf b/resources/fillable-char-sheetstyle-4-5-spells.pdf new file mode 100644 index 000000000..0bb70eb12 Binary files /dev/null and b/resources/fillable-char-sheetstyle-4-5-spells.pdf differ diff --git a/resources/fillable-char-sheetstyle-4-6-spells.pdf b/resources/fillable-char-sheetstyle-4-6-spells.pdf new file mode 100644 index 000000000..773f78eb4 Binary files /dev/null and b/resources/fillable-char-sheetstyle-4-6-spells.pdf differ diff --git a/resources/public/blank.html b/resources/public/blank.html deleted file mode 100644 index ffbf571ef..000000000 --- a/resources/public/blank.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - OrcPub - - - - -
- - - - - - - - diff --git a/resources/public/css/compiled/styles.css b/resources/public/css/compiled/styles.css deleted file mode 100644 index 5d8b646eb..000000000 --- a/resources/public/css/compiled/styles.css +++ /dev/null @@ -1 +0,0 @@ -.character-builder-header{margin-bottom:19px}.registration-content{width:785px;min-height:600px}.registration-input{min-width:438px}p{margin:10px 0}a,a:visited{color:#f0a100}select{font-family:Open Sans,sans-serif;cursor:pointer}*:focus{outline:0}.sticky-header{top:0;box-shadow:0 2px 6px 0 rgba(0,0,0,0.5);z-index:100;display:none;background-color:#313a4d}.container{display:flex;justify-content:center}.content{max-width:1440px;width:100%}.app-header{background-color:black;background-image:url(/../../image/header-background.jpg);background-position:right center;background-size:cover;height:227px}.header-tab{background-color:rgba(0,0,0,0.5)}.header-tab.mobile{width:30px}.header-tab.mobile .title{display:none}.header-tab.mobile img{height:24px;width:24px}.item-list{border-top:1px solid rgba(255,255,255,0.5)}.item-list-item{border-bottom:1px solid rgba(255,255,255,0.5)}.app-header-bar{min-height:81px;background-color:rgba(0,0,0,0.25)}.builder-column{display:none;margin:0 5px}.stepper-column{margin-right:-10px}table.striped tr:nth-child(even){background-color:rgba(255,255,255,0.1)}.builder-option{border-width:1px;border-style:solid;border-color:rgba(255,255,255,0.5);border-radius:5px;padding:10px;margin-top:5px;font-weight:normal}.builder-tabs{display:flex;padding:10px;text-transform:uppercase;font-weight:600}.builder-tab{flex-grow:1;padding-bottom:13px;text-align:center;cursor:pointer;border-bottom:5px solid rgba(72,72,72,0.37)}.builder-tab .builder-tab-text{opacity:.2}.selected-builder-tab{border-bottom-color:#f1a20f}.selected-builder-tab .builder-tab-text{opacity:1}.collapsed-list-builder-option{padding:1px}.disabled-builder-option{color:rgba(255,255,255,0.5);border-color:rgba(255,255,255,0.25);cursor:auto}.selectable-builder-option:hover{border-color:#f1a20f;box-shadow:0 2px 6px 0 rgba(0,0,0,0.5);cursor:pointer}.builder-selector{padding:5px;font-size:14px;margin-top:10px}.builder-selector-header{font-size:18px;font-weight:normal}.builder-option-dropdown{background-color:transparent;width:100%;cursor:pointer;border:1px solid white;color:white;-webkit-appearance:menulist;-moz-appearance:menulist;appearance:menulist}.builder-option-dropdown:active,.builder-option-dropdown:focus{outline:0}.builder-dropdown-item{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:black}.selected-builder-option{border-width:3px;border-color:white;font-weight:bold}.remove-item-button{color:#f0a100;font-size:16px;margin-left:5px;cursor:pointer}.add-item-button{margin-top:19px;color:#f0a100;font-weight:600;text-decoration:underline;cursor:pointer}.list-selector-option{display:flex;align-items:center}.expand-collapse-button{font-size:12px;max-width:100px;margin-left:10px;color:#f0a100;text-decoration:underline;cursor:pointer;text-align:right}.fa-caret-square-o-down{color:#f0a100}.expand-collapse-button:hover{color:#f0a100}.abilities-polygon{transition:points 2s;-webkit-transition:points 2s}.display-section-qualifier-text{font-size:12px;margin-left:5px}.form-button{text-transform:uppercase;color:white;font-size:12px;font-weight:600;cursor:pointer;background-image:linear-gradient(to bottom,#f1a20f,#dbab50);padding:10px 15px;border:0;border-radius:5px}.form-button:hover{box-shadow:0 2px 6px 0 rgba(0,0,0,0.5)}.form-button.disabled{opacity:.5;cursor:not-allowed}.form-button.disabled:hover{box-shadow:none}.link-button{text-transform:uppercase;color:#f0a100;font-size:12px;background-color:transparent;cursor:pointer;padding:10px 15px;text-decoration:underline;border:0;border-radius:5px}.link-button.disabled{opacity:.5;cursor:not-allowed}.field{margin-top:30px}.field-label{font-size:14px}.personality-label{font-size:18px}.input{width:100%;border:1px solid white;padding:10px;margin-top:5px;font-size:14px;background-color:transparent;border-radius:5px;box-sizing:border-box;display:block;color:white}.checkbox-parent{display:flex;padding:11px 0;align-items:center}.checkbox{width:16px;height:16px;box-shadow:0 1px 0 0 #f0a100;background-color:white;cursor:pointer}.checkbox .fa-check{font-size:14px;margin:1px}.checkbox.checked.disabled{background-color:rgba(255,255,255,0.37);cursor:not-allowed}.checkbox-text{margin-left:5px}#selection-stepper{transition:top 2s ease-in-out;width:240px;position:relative;top:0}.selection-stepper-inner{position:absolute}.selection-stepper-main{width:200px;border:1px solid white;border-radius:5px;padding:10px;background-color:#1a1e28;box-shadow:0 2px 6px 0 rgba(0,0,0,0.5)}.selection-stepper-title{font-size:18px;color:#f0a100}.selection-stepper-help{font-size:14px;font-weight:100}.selection-stepper-footer{justify-content:flex-end}.option-header{display:flex;justify-content:space-between;align-items:center}.app.light-theme{background-image:linear-gradient(182deg,#fff,#ddd)}.app.light-theme .item-list{border-top:1px solid rgba(0,0,0,0.5)}.app.light-theme .link-button{color:#363636}.app.light-theme .item-list-item{border-bottom:1px solid rgba(0,0,0,0.5)}.app.light-theme .main-text-color{color:#363636;fill:#363636}.app.light-theme .stroke-color{stroke:#363636}.app.light-theme .input{width:100%;border:1px solid #282828;padding:10px;margin-top:5px;font-size:14px;background-color:transparent;border-radius:5px;box-sizing:border-box;display:block;color:black}.app.light-theme .form-button{background-image:linear-gradient(to bottom,#33658a,#33658a)}.app.light-theme .orange{color:rgba(0,0,0,0.8)}.app.light-theme .b-orange{border-color:rgba(0,0,0,0.6)}.app.light-theme .text-shadow{text-shadow:none}.app.light-theme .bg-light{background-color:rgba(0,0,0,0.4)}.app.light-theme .bg-lighter{background-color:rgba(0,0,0,0.15)}.app.light-theme .b-color-gray{border-color:rgba(0,0,0,0.3)}.app.light-theme .builder-option-dropdown{border:1px solid #282828;color:#282828}.app.light-theme .builder-option-dropdown:active,.app.light-theme .builder-option-dropdown:focus{outline:0}.app.light-theme .builder-dropdown-item{background-color:white;color:#282828}.app.light-theme .sticky-header{background-color:white}.app.light-theme table.striped tr:nth-child(even){background-color:rgba(0,0,0,0.1)}.m-l--1{margin-left:-1px !important}.m-l-0{margin-left:0 !important}.m-l-1{margin-left:1px !important}.m-l-2{margin-left:2px !important}.m-l-3{margin-left:3px !important}.m-l-4{margin-left:4px !important}.m-l-5{margin-left:5px !important}.m-l-6{margin-left:6px !important}.m-l-7{margin-left:7px !important}.m-l-8{margin-left:8px !important}.m-l-9{margin-left:9px !important}.m-l-10{margin-left:10px !important}.m-l-15{margin-left:15px !important}.m-l-20{margin-left:20px !important}.m-l-25{margin-left:25px !important}.m-l-30{margin-left:30px !important}.m-l-35{margin-left:35px !important}.m-l-40{margin-left:40px !important}.m-l-45{margin-left:45px !important}.m-l-50{margin-left:50px !important}.m-t-0{margin-top:0 !important}.m-t-1{margin-top:1px !important}.m-t-2{margin-top:2px !important}.m-t-3{margin-top:3px !important}.m-t-4{margin-top:4px !important}.m-t-5{margin-top:5px !important}.m-t-6{margin-top:6px !important}.m-t-7{margin-top:7px !important}.m-t-8{margin-top:8px !important}.m-t-9{margin-top:9px !important}.m-t-21{margin-top:21px !important}.m-t-10{margin-top:10px !important}.m-t-15{margin-top:15px !important}.m-t-20{margin-top:20px !important}.m-t-25{margin-top:25px !important}.w-12{width:12px !important}.w-14{width:14px !important}.w-15{width:15px !important}.w-18{width:18px !important}.w-20{width:20px !important}.w-24{width:24px !important}.w-32{width:32px !important}.w-36{width:36px !important}.w-40{width:40px !important}.w-48{width:48px !important}.w-50{width:50px !important}.w-60{width:60px !important}.w-70{width:70px !important}.w-80{width:80px !important}.w-85{width:85px !important}.w-90{width:90px !important}.w-100{width:100px !important}.w-110{width:110px !important}.w-120{width:120px !important}.w-200{width:200px !important}.w-220{width:220px !important}.w-250{width:250px !important}.w-300{width:300px !important}.w-500{width:500px !important}.w-1440{width:1440px !important}.f-s-10{font-size:10px}.f-s-11{font-size:11px}.f-s-12{font-size:12px !important}.f-s-14{font-size:14px !important}.f-s-16{font-size:16px !important}.f-s-18{font-size:18px !important}.f-s-20{font-size:20px !important}.f-s-24{font-size:24px !important}.f-s-28{font-size:28px}.f-s-32{font-size:32px !important}.f-s-36{font-size:36px !important}.f-s-48{font-size:48px !important}.sans{font-family:Open Sans,sans-serif}.flex{display:flex}.inline-block{display:inline-block}.flex-column{flex-direction:column}.list-style-disc{list-style-type:disc;list-style-position:inside}.f-w-bold{font-weight:bold}.flex-grow-1{flex-grow:1}.flex-basis-50-p{flex-basis:50%}.i{font-style:italic}.f-w-n{font-weight:normal}.f-w-b{font-weight:bold}.f-w-600{font-weight:600}.l-h-19{line-height:19px}.l-h-20{line-height:20px}.m-r--10{margin-right:-10px}.m-r--5{margin-right:-5px}.m-r-2{margin-right:2px}.m-r-5{margin-right:5px}.m-r-10{margin-right:10px}.m-r-18{margin-right:18px}.m-r-20{margin-right:20px}.m-r-30{margin-right:30px}.m-r-80{margin-right:80px}.m-t--10{margin-top:-10px}.m-t--20{margin-top:-20px}.m-t--5{margin-top:-5px}.m-t-2{margin-top:2px}.m-t-20{margin-top:20px}.m-t-30{margin-top:30px}.m-t-40{margin-top:40px}.m-t-21{margin-top:21px}.opacity-0{opacity:0}.opacity-1{opacity:.1}.opacity-2{opacity:.2}.opacity-5{opacity:.5}.opacity-6{opacity:.6}.opacity-7{opacity:.7}.opacity-9{opacity:.9}.m-b--2{margin-bottom:-2px}.m-b--1{margin-bottom:-1px}.m-b-0-last:last-child{margin-bottom:0}.m-b-2{margin-bottom:2px}.m-b-5{margin-bottom:5px}.m-b-10{margin-bottom:10px}.m-b-16{margin-bottom:16px}.m-b-19{margin-bottom:19px}.m-b-20{margin-bottom:20px}.m-b-30{margin-bottom:30px}.m-b-40{margin-bottom:40px}.m-l--10{margin-left:-10px}.m-l--5{margin-left:-5px}.m-l-30{margin-left:30px}.m-5{margin:5px}.text-shadow{text-shadow:1px 2px 1px black}.white-text-shadow{text-shadow:1px 2px 1px white}.slight-text-shadow{text-shadow:1px 1px 1px rgba(0,0,0,0.8)}.hover-shadow:hover,.shadow{box-shadow:0 2px 6px 0 rgba(0,0,0,0.5)}.hover-no-shadow:hover{box-shadow:none}.hover-underline:hover{text-decoration:underline}.orange-shadow{box-shadow:0 1px 0 0 #f0a100}.t-a-c{text-align:center}.t-a-l{text-align:left}.t-a-r{text-align:right}.justify-cont-s-b{justify-content:space-between}.justify-cont-s-a{justify-content:space-around}.justify-cont-c{justify-content:center}.justify-cont-end{justify-content:flex-end}.align-items-c{align-items:center}.align-items-t{align-items:flex-start}.align-items-end{align-items:flex-end}.flex-wrap{flex-wrap:wrap}.w-auto{width:auto}.w-40-p{width:40%}.w-50-p{width:50%}.w-60-p{width:60%}.w-100-p{width:100%}.h-0{height:0}.h-12{height:12px}.h-14{height:14px}.h-15{height:15px}.h-18{height:18px}.h-20{height:20px}.h-24{height:24px}.h-25{height:25px}.h-32{height:32px}.h-36{height:36px}.h-40{height:40px}.h-48{height:48px}.h-60{height:60px}.h-72{height:72px}.h-120{height:120px}.h-200{height:200px}.h-800{height:800px}.h-100-p{height:100%}.overflow-auto{overflow:auto}.posn-rel{position:relative}.posn-abs{position:absolute}.posn-fixed{position:fixed}.main-text-color{color:white;fill:white}.stroke-color{stroke:white}.white{color:white}.black{color:#191919}.orange{color:#f0a100}.orange a,.orange a:visited{color:#f0a100}.green{color:#70a800}.green a,.green a:visited{color:#70a800}.red{color:#9a031e}.red a,.red a:visited{color:#9a031e}.uppercase{text-transform:uppercase}.bg-trans{background-color:transparent}.bg-white{background-color:white}.bg-slight-white{background-color:rgba(255,255,255,0.05)}.no-border{border:0}.underline{text-decoration:underline}.no-text-decoration{text-decoration:none}.p-t-0{padding-top:0}.p-t-2{padding-top:2px}.p-t-3{padding-top:3px}.p-t-4{padding-top:4px}.p-t-5{padding-top:5px}.p-t-10{padding-top:10px}.p-t-20{padding-top:20px}.p-b-5{padding-bottom:5px}.p-b-10{padding-bottom:10px}.p-b-20{padding-bottom:20px}.p-b-40{padding-bottom:40px}.p-0{padding:0}.p-1{padding:1px}.p-2{padding:2px}.p-5{padding:5px}.p-10{padding:10px}.p-20{padding:20px}.p-30{padding:30px}.p-5-10{padding:5px 10px}.p-l-0{padding-left:0}.p-l-5{padding-left:5px}.p-l-10{padding-left:10px}.p-l-15{padding-left:15px}.p-l-20{padding-left:20px}.p-r-5{padding-right:5px}.p-r-10{padding-right:10px}.p-r-20{padding-right:20px}.p-r-40{padding-right:40px}.b-rad-50-p{border-radius:50%}.b-rad-5{border-radius:5px}.b-1{border:1px solid}.b-3{border:3px solid}.b-b-2{border-bottom:2px solid}.b-w-3{border-width:3px}.b-w-5{border-width:5px}.b-color-gray{border-color:rgba(255,255,255,0.2)}ul.list-style-disc{list-style-type:disc}.hidden{display:none}@keyframes fade-out{from{opacity:1;height:100%}50%{opacity:0;height:100%}to{height:0}}.pointer{cursor:pointer}.cursor-disabled{cursor:not-allowed}.c-f4692a{color:#f4692a}.c-f32e50{color:#f32e50}.c-b35c95{color:#b35c95}.c-47eaf8{color:#47eaf8}.c-bbe289{color:#bbe289}.c-f9b747{color:#f9b747}.b-orange{border-color:#f0a100}.b-red{border-color:#9a031e}.b-gray{border-color:rgba(72,72,72,0.37)}.hover-slight-white:hover{background-color:#2c3445;opacity:.2}.hover-opacity-full:hover{opacity:1.0}.bg-light{background-color:rgba(72,72,72,0.2)}.bg-lighter{background-color:rgba(0,0,0,0.15)}.bg-orange{background-color:#f0a100}.bg-red{background-color:#9a031e}.bg-green{background-color:#70a800}.fade-out{animation-name:fade-out;animation-duration:5s}.no-appearance{-webkit-appearance:none;-moz-appearance:none;appearance:none}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}.visible-xs-block,.visible-xs-inline,.visible-xs-inline-block,.visible-sm-block,.visible-sm-inline,.visible-sm-inline-block,.visible-md-block,.visible-md-inline,.visible-md-inline-block,.visible-lg-block,.visible-lg-inline,.visible-lg-inline-block{display:none !important}@media(max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table !important}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media(max-width:767px){.visible-xs-block{display:block !important}}@media(max-width:767px){.visible-xs-inline{display:inline !important}}@media(max-width:767px){.visible-xs-inline-block{display:inline-block !important}}@media(min-width:768px) and (max-width:991px){.visible-sm{display:block !important;display:table !important;display:table-row !important;display:table-cell !important}table.visible-sm tr.visible-sm th.visible-sm td.visible-sm}@media(min-width:768px) and (max-width:991px){.visible-sm-block{display:block !important}}@media(min-width:768px) and (max-width:991px){.visible-sm-inline{display:inline !important}}@media(min-width:768px) and (max-width:991px){.visible-sm-inline-block{display:inline-block !important}}@media(min-width:992px) and (max-width:1199px){.visible-md{display:block !important;display:table !important;display:table-row !important;display:table-cell !important}table.visible-md tr.visible-md th.visible-md td.visible-md}@media(min-width:992px) and (max-width:1199px){.visible-md-block{display:block !important}}@media(min-width:992px) and (max-width:1199px){.visible-md-inline{display:inline !important}}@media(min-width:992px) and (max-width:1199px){.visible-md-inline-block{display:inline-block !important}}@media(min-width:1200px){.visible-lg{display:block !important;display:table !important;display:table-row !important;display:table-cell !important}table.visible-lg tr.visible-lg th.visible-lg td.visible-lg}@media(min-width:1200px){.visible-lg-inline{display:inline !important}}@media(min-width:1200px){.visible-lg-inline-block{display:inline-block !important}}@media(max-width:767px){.hidden-xs{display:none !important}}@media(min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media(min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media(min-width:1200px){.hidden-lg{display:none !important}}@media print{.visible-print{display:block !important}th.visible-print,td.visible-print{display:table-cell !important}table.visible-print{display:table !important}tr.visible-print{display:table-row !important}}.visible-print-block{display:none !important}@media print{.visible-print-block{display:block !important}}.visible-print-inline{display:none !important}@media print{.visible-print-inline{display:inline !important}}.visible-print-inline-block{display:none !important}@media print{.visible-print-inline-block{display:inline-block !important}}@media print{.hidden-print{display:none !important}}@media(max-width:767px){.user-icon{display:none}.list-character-summary{font-size:18px}.character-summary{flex-wrap:wrap}.app-header{height:auto;background-image:none;background-color:rgba(0,0,0,0.3);min-height:0}.app-header-bar{min-height:50px;backdrop-filter:none;-webkit-backdrop-filter:none}.content{width:100%}.header-button-text,.header-links{display:none}}@media(max-width:1199px){.registration-image{display:none}.registration-content{width:100%;height:100%}.registration-input{width:100%}} \ No newline at end of file diff --git a/resources/public/css/compiled/test.css b/resources/public/css/compiled/test.css deleted file mode 100644 index 23d89649b..000000000 --- a/resources/public/css/compiled/test.css +++ /dev/null @@ -1 +0,0 @@ -.red {color: red} diff --git a/resources/public/css/cookiestyles.css b/resources/public/css/cookiestyles.css new file mode 100644 index 000000000..96b6171f6 --- /dev/null +++ b/resources/public/css/cookiestyles.css @@ -0,0 +1 @@ +.palette1{background-image:linear-gradient( 234deg,rgb(39,38,38) 0%,#394245 90% )!important;color:#fff!important}.palette1 .policylink{color:#ccc!important}.palette1 .spopupbtnok{color:#2a2a2b!important;background-color:#ff0!important}.palette1.wire .spopupbtnok{border:1px solid!important;border-color:#ff0!important;background-color:transparent!important;color:#ff0!important}.palette2{background-color:#edeff5!important;color:#838391!important}.palette2 .spopupbtnok{color:#fff!important;background-color:#1e90ff!important}.palette2.wire .spopupbtnok{border:1px solid!important;border-color:#1e90ff!important;background-color:transparent!important;color:#1e90ff!important}.palette3{background-image:linear-gradient( 234deg,#438ed9 30%,#5a94ce 90% )!important;color:#fff!important}.palette3 .policylink{color:#d3e4fe}.palette3 .spopupbtnok{background-color:#0b5eb1!important;color:#fff!important}.palette3.wire .spopupbtnok{border:1px solid!important;border-color:navy!important;background-color:transparent!important;color:#fff!important}.palette4{background-image:linear-gradient( 234deg,#8c869f 60%,#bcbcc6 90% )!important;color:#fff!important}.palette4 .policylink{color:wheat!important}.palette4 .spopupbtnok{background-color:#f5f5f5;color:#000!important;border:none}.palette4.wire .spopupbtnok{border:1px solid!important;border-color:snow!important;background-color:transparent!important;color:snow!important}.palette5{background-color:#000;color:#0f0}.palette5 .policylink{color:#0c0}.palette5 .spopupbtnok{background-color:#0f0!important;color:#000!important}.palette5.wire .spopupbtnok{border:1px solid!important;border-color:#0f0!important;background-color:transparent!important;color:#0f0!important}.palette6{background-color:#252e39;color:#fff}.palette6 .policylink{color:#d3d5d7}.palette6 .spopupbtnok{background-color:#14a7d0!important;color:#fff!important}.palette6.wire .spopupbtnok{border:1px solid!important;border-color:#14a7d0!important;background-color:transparent!important;color:#14a7d0!important}.palette7{background-color:#eb6c44;color:#fff}.palette7 .policylink{color:#fbe2da}.palette7 .spopupbtnok{background-color:#f5d948!important;color:#000!important}.palette7.wire .spopupbtnok{border:1px solid!important;border-color:#f5d948!important;background-color:transparent!important;color:#f5d948!important}.palette8{background-color:#64386b;color:#ffcdfd}.palette8 .policylink{color:#e0afe0}.palette8 .spopupbtnok{background-color:#f8a8ff!important;color:#3f0045!important}.palette8.wire .spopupbtnok{border:1px solid!important;border-color:#f8a8ff!important;background-color:transparent!important;color:#f8a8ff!important}.window{position:fixed!important;overflow:hidden!important;-webkit-box-sizing:border-box!important;box-sizing:border-box;font-family:Helvetica,Calibri,Arial,sans-serif!important;font-size:16px!important;line-height:1.5em!important;display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-ms-flex-wrap:nowrap!important;flex-wrap:nowrap!important;z-index:9999!important;opacity:1!important;-webkit-transition:opacity 1s ease!important;transition:opacity 1s ease!important}.floating{padding:2em!important;width:24em!important;-webkit-box-orient:vertical!important;-webkit-box-direction:normal!important;-ms-flex-direction:column!important;flex-direction:column!important}.window.banner{padding:1em 1.8em!important;width:100%!important;border-radius:5px!important;-webkit-box-orient:horizontal!important;-webkit-box-direction:normal!important;-ms-flex-direction:row!important;flex-direction:row!important}.policylink{opacity:.8!important;display:inline-block!important;padding:.2em!important;text-decoration:underline!important}.policylink:hover{opacity:1!important;text-decoration:none!important;transition:1.5s!important}.spopupbtnok{opacity:.9!important;display:block!important;padding:.4em .8em!important;font-size:.9em!important;text-decoration:none!important;font-weight:700!important;border-width:2px!important;text-align:center!important;white-space:nowrap!important}@media screen and (max-width:550px){.window.floating.left{display:flex;right:0!important;left:0!important;bottom:0;width:100%!important}}.window.floating.left{bottom:0!important;left:3em;border-top-left-radius:.5em!important;border-top-right-radius:.5em!important}@media screen and (max-width:550px){.window.floating.right{display:flex;right:0!important;left:0!important;bottom:0;width:100%!important}}.window.floating.right{bottom:0!important;right:3em;border-top-left-radius:.5em!important;border-top-right-radius:.5em!important}.floating>.policylink{margin-bottom:1em!important}.floating .message{display:block!important;margin-bottom:1em!important}.window.floating .compliance{-webkit-box-flex:1!important;-ms-flex:1 0 auto!important;flex:1 0 auto!important}.window.banner{-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important}.banner.top{left:0!important;right:0!important;top:0!important}.banner.bottom{left:0!important;right:0!important;bottom:0!important}.banner .message{display:block!important;-webkit-box-flex:1!important;-ms-flex:1 1 auto!important;flex:1 1 auto!important;max-width:100%!important;margin-right:1em!important}.compliance{display:-webkit-box!important;display:-ms-flexbox!important;display:flex!important;-webkit-box-align:center!important;-ms-flex-align:center!important;align-items:center!important;-ms-flex-line-pack:justify!important;align-content:space-between!important}.floating .compliance>.spopupbtnok{-webkit-box-flex:1!important;-ms-flex:1!important;flex:1!important}.spopupbtnok+.spopupbtnok{margin-left:.5em!important}.floating.classic{padding:1.2em!important;border-radius:5px!important}.floating.classic .compliance{text-align:center!important;display:inline!important;-webkit-box-flex:0!important;-ms-flex:none!important;flex:none!important}.classic .spopupbtnok{border-radius:5px!important}.classic .spopupbtnok:last-child{min-width:140px!important}.floating.classic .spopupbtnok{display:inline-block!important}.edgeless.window{padding:0!important}.floating.edgeless .message{margin:2em!important;margin-bottom:1.5em!important}.banner.edgeless .spopupbtnok{margin:0!important;padding:.8em 1.8em!important;height:100%!important}.banner.edgeless .message{margin-left:1em!important}.floating.edgeless .spopupbtnok+.spopupbtnok{margin-left:0!important}.spopupbtnok:hover{opacity:1!important;cursor:pointer!important} \ No newline at end of file diff --git a/resources/public/css/style.css b/resources/public/css/style.css deleted file mode 100644 index 294c1df2c..000000000 --- a/resources/public/css/style.css +++ /dev/null @@ -1,9 +0,0 @@ - - -/* remember to define visible focus styles! -:focus { - outline: ?????; -} */ - -/* remember to highlight inserts somehow! */ - diff --git a/resources/public/favicon/browserconfig.xml b/resources/public/favicon/browserconfig.xml new file mode 100644 index 000000000..42a62a8d8 --- /dev/null +++ b/resources/public/favicon/browserconfig.xml @@ -0,0 +1,11 @@ + + + + + + + + #FFFFFF + + + \ No newline at end of file diff --git a/resources/public/favicon/favicon-114.png b/resources/public/favicon/favicon-114.png new file mode 100644 index 000000000..4c91f7aab Binary files /dev/null and b/resources/public/favicon/favicon-114.png differ diff --git a/resources/public/favicon/favicon-120.png b/resources/public/favicon/favicon-120.png new file mode 100644 index 000000000..42f3f9998 Binary files /dev/null and b/resources/public/favicon/favicon-120.png differ diff --git a/resources/public/favicon/favicon-144.png b/resources/public/favicon/favicon-144.png new file mode 100644 index 000000000..d75776d52 Binary files /dev/null and b/resources/public/favicon/favicon-144.png differ diff --git a/resources/public/favicon/favicon-150.png b/resources/public/favicon/favicon-150.png new file mode 100644 index 000000000..7de60605a Binary files /dev/null and b/resources/public/favicon/favicon-150.png differ diff --git a/resources/public/favicon/favicon-152.png b/resources/public/favicon/favicon-152.png new file mode 100644 index 000000000..5b698b4d1 Binary files /dev/null and b/resources/public/favicon/favicon-152.png differ diff --git a/resources/public/favicon/favicon-16.png b/resources/public/favicon/favicon-16.png new file mode 100644 index 000000000..16c9b38b4 Binary files /dev/null and b/resources/public/favicon/favicon-16.png differ diff --git a/resources/public/favicon/favicon-160.png b/resources/public/favicon/favicon-160.png new file mode 100644 index 000000000..565ae4ba3 Binary files /dev/null and b/resources/public/favicon/favicon-160.png differ diff --git a/resources/public/favicon/favicon-180.png b/resources/public/favicon/favicon-180.png new file mode 100644 index 000000000..ed2d74ed3 Binary files /dev/null and b/resources/public/favicon/favicon-180.png differ diff --git a/resources/public/favicon/favicon-192.png b/resources/public/favicon/favicon-192.png new file mode 100644 index 000000000..e3c4b313e Binary files /dev/null and b/resources/public/favicon/favicon-192.png differ diff --git a/resources/public/favicon/favicon-310.png b/resources/public/favicon/favicon-310.png new file mode 100644 index 000000000..6961a895e Binary files /dev/null and b/resources/public/favicon/favicon-310.png differ diff --git a/resources/public/favicon/favicon-32.png b/resources/public/favicon/favicon-32.png new file mode 100644 index 000000000..e11be8f70 Binary files /dev/null and b/resources/public/favicon/favicon-32.png differ diff --git a/resources/public/favicon/favicon-57.png b/resources/public/favicon/favicon-57.png new file mode 100644 index 000000000..72d5d154e Binary files /dev/null and b/resources/public/favicon/favicon-57.png differ diff --git a/resources/public/favicon/favicon-60.png b/resources/public/favicon/favicon-60.png new file mode 100644 index 000000000..430b00d88 Binary files /dev/null and b/resources/public/favicon/favicon-60.png differ diff --git a/resources/public/favicon/favicon-64.png b/resources/public/favicon/favicon-64.png new file mode 100644 index 000000000..cee5afa70 Binary files /dev/null and b/resources/public/favicon/favicon-64.png differ diff --git a/resources/public/favicon/favicon-70.png b/resources/public/favicon/favicon-70.png new file mode 100644 index 000000000..b75cc17a1 Binary files /dev/null and b/resources/public/favicon/favicon-70.png differ diff --git a/resources/public/favicon/favicon-72.png b/resources/public/favicon/favicon-72.png new file mode 100644 index 000000000..856dbe445 Binary files /dev/null and b/resources/public/favicon/favicon-72.png differ diff --git a/resources/public/favicon/favicon-76.png b/resources/public/favicon/favicon-76.png new file mode 100644 index 000000000..89c9af16b Binary files /dev/null and b/resources/public/favicon/favicon-76.png differ diff --git a/resources/public/favicon/favicon-96.png b/resources/public/favicon/favicon-96.png new file mode 100644 index 000000000..0f06db202 Binary files /dev/null and b/resources/public/favicon/favicon-96.png differ diff --git a/resources/public/favicon/favicon.ico b/resources/public/favicon/favicon.ico new file mode 100644 index 000000000..6b71e6b34 Binary files /dev/null and b/resources/public/favicon/favicon.ico differ diff --git a/resources/public/favicon/faviconit-instructions.txt b/resources/public/favicon/faviconit-instructions.txt new file mode 100644 index 000000000..5da032dd3 --- /dev/null +++ b/resources/public/favicon/faviconit-instructions.txt @@ -0,0 +1,25 @@ +thanks for using faviconit! +copy the files to your site and add this code inside the HTML tag: + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/resources/public/font-awesome-4.7.0/HELP-US-OUT.txt b/resources/public/font-awesome-4.7.0/HELP-US-OUT.txt deleted file mode 100644 index 83d083dd7..000000000 --- a/resources/public/font-awesome-4.7.0/HELP-US-OUT.txt +++ /dev/null @@ -1,7 +0,0 @@ -I hope you love Font Awesome. If you've found it useful, please do me a favor and check out my latest project, -Fort Awesome (https://fortawesome.com). It makes it easy to put the perfect icons on your website. Choose from our awesome, -comprehensive icon sets or copy and paste your own. - -Please. Check it out. - --Dave Gandy diff --git a/resources/public/font-awesome-4.7.0/css/font-awesome.css b/resources/public/font-awesome-4.7.0/css/font-awesome.css deleted file mode 100644 index ee906a819..000000000 --- a/resources/public/font-awesome-4.7.0/css/font-awesome.css +++ /dev/null @@ -1,2337 +0,0 @@ -/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */ -/* FONT PATH - * -------------------------- */ -@font-face { - font-family: 'FontAwesome'; - src: url('../fonts/fontawesome-webfont.eot?v=4.7.0'); - src: url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'), url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'), url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'), url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'), url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg'); - font-weight: normal; - font-style: normal; -} -.fa { - display: inline-block; - font: normal normal normal 14px/1 FontAwesome; - font-size: inherit; - text-rendering: auto; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} -/* makes the font 33% larger relative to the icon container */ -.fa-lg { - font-size: 1.33333333em; - line-height: 0.75em; - vertical-align: -15%; -} -.fa-2x { - font-size: 2em; -} -.fa-3x { - font-size: 3em; -} -.fa-4x { - font-size: 4em; -} -.fa-5x { - font-size: 5em; -} -.fa-fw { - width: 1.28571429em; - text-align: center; -} -.fa-ul { - padding-left: 0; - margin-left: 2.14285714em; - list-style-type: none; -} -.fa-ul > li { - position: relative; -} -.fa-li { - position: absolute; - left: -2.14285714em; - width: 2.14285714em; - top: 0.14285714em; - text-align: center; -} -.fa-li.fa-lg { - left: -1.85714286em; -} -.fa-border { - padding: .2em .25em .15em; - border: solid 0.08em #eeeeee; - border-radius: .1em; -} -.fa-pull-left { - float: left; -} -.fa-pull-right { - float: right; -} -.fa.fa-pull-left { - margin-right: .3em; -} -.fa.fa-pull-right { - margin-left: .3em; -} -/* Deprecated as of 4.4.0 */ -.pull-right { - float: right; -} -.pull-left { - float: left; -} -.fa.pull-left { - margin-right: .3em; -} -.fa.pull-right { - margin-left: .3em; -} -.fa-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; -} -.fa-pulse { - -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); -} -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -.fa-rotate-90 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=1)"; - -webkit-transform: rotate(90deg); - -ms-transform: rotate(90deg); - transform: rotate(90deg); -} -.fa-rotate-180 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2)"; - -webkit-transform: rotate(180deg); - -ms-transform: rotate(180deg); - transform: rotate(180deg); -} -.fa-rotate-270 { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=3)"; - -webkit-transform: rotate(270deg); - -ms-transform: rotate(270deg); - transform: rotate(270deg); -} -.fa-flip-horizontal { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)"; - -webkit-transform: scale(-1, 1); - -ms-transform: scale(-1, 1); - transform: scale(-1, 1); -} -.fa-flip-vertical { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)"; - -webkit-transform: scale(1, -1); - -ms-transform: scale(1, -1); - transform: scale(1, -1); -} -:root .fa-rotate-90, -:root .fa-rotate-180, -:root .fa-rotate-270, -:root .fa-flip-horizontal, -:root .fa-flip-vertical { - filter: none; -} -.fa-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; -} -.fa-stack-1x, -.fa-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; -} -.fa-stack-1x { - line-height: inherit; -} -.fa-stack-2x { - font-size: 2em; -} -.fa-inverse { - color: #ffffff; -} -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ -.fa-glass:before { - content: "\f000"; -} -.fa-music:before { - content: "\f001"; -} -.fa-search:before { - content: "\f002"; -} -.fa-envelope-o:before { - content: "\f003"; -} -.fa-heart:before { - content: "\f004"; -} -.fa-star:before { - content: "\f005"; -} -.fa-star-o:before { - content: "\f006"; -} -.fa-user:before { - content: "\f007"; -} -.fa-film:before { - content: "\f008"; -} -.fa-th-large:before { - content: "\f009"; -} -.fa-th:before { - content: "\f00a"; -} -.fa-th-list:before { - content: "\f00b"; -} -.fa-check:before { - content: "\f00c"; -} -.fa-remove:before, -.fa-close:before, -.fa-times:before { - content: "\f00d"; -} -.fa-search-plus:before { - content: "\f00e"; -} -.fa-search-minus:before { - content: "\f010"; -} -.fa-power-off:before { - content: "\f011"; -} -.fa-signal:before { - content: "\f012"; -} -.fa-gear:before, -.fa-cog:before { - content: "\f013"; -} -.fa-trash-o:before { - content: "\f014"; -} -.fa-home:before { - content: "\f015"; -} -.fa-file-o:before { - content: "\f016"; -} -.fa-clock-o:before { - content: "\f017"; -} -.fa-road:before { - content: "\f018"; -} -.fa-download:before { - content: "\f019"; -} -.fa-arrow-circle-o-down:before { - content: "\f01a"; -} -.fa-arrow-circle-o-up:before { - content: "\f01b"; -} -.fa-inbox:before { - content: "\f01c"; -} -.fa-play-circle-o:before { - content: "\f01d"; -} -.fa-rotate-right:before, -.fa-repeat:before { - content: "\f01e"; -} -.fa-refresh:before { - content: "\f021"; -} -.fa-list-alt:before { - content: "\f022"; -} -.fa-lock:before { - content: "\f023"; -} -.fa-flag:before { - content: "\f024"; -} -.fa-headphones:before { - content: "\f025"; -} -.fa-volume-off:before { - content: "\f026"; -} -.fa-volume-down:before { - content: "\f027"; -} -.fa-volume-up:before { - content: "\f028"; -} -.fa-qrcode:before { - content: "\f029"; -} -.fa-barcode:before { - content: "\f02a"; -} -.fa-tag:before { - content: "\f02b"; -} -.fa-tags:before { - content: "\f02c"; -} -.fa-book:before { - content: "\f02d"; -} -.fa-bookmark:before { - content: "\f02e"; -} -.fa-print:before { - content: "\f02f"; -} -.fa-camera:before { - content: "\f030"; -} -.fa-font:before { - content: "\f031"; -} -.fa-bold:before { - content: "\f032"; -} -.fa-italic:before { - content: "\f033"; -} -.fa-text-height:before { - content: "\f034"; -} -.fa-text-width:before { - content: "\f035"; -} -.fa-align-left:before { - content: "\f036"; -} -.fa-align-center:before { - content: "\f037"; -} -.fa-align-right:before { - content: "\f038"; -} -.fa-align-justify:before { - content: "\f039"; -} -.fa-list:before { - content: "\f03a"; -} -.fa-dedent:before, -.fa-outdent:before { - content: "\f03b"; -} -.fa-indent:before { - content: "\f03c"; -} -.fa-video-camera:before { - content: "\f03d"; -} -.fa-photo:before, -.fa-image:before, -.fa-picture-o:before { - content: "\f03e"; -} -.fa-pencil:before { - content: "\f040"; -} -.fa-map-marker:before { - content: "\f041"; -} -.fa-adjust:before { - content: "\f042"; -} -.fa-tint:before { - content: "\f043"; -} -.fa-edit:before, -.fa-pencil-square-o:before { - content: "\f044"; -} -.fa-share-square-o:before { - content: "\f045"; -} -.fa-check-square-o:before { - content: "\f046"; -} -.fa-arrows:before { - content: "\f047"; -} -.fa-step-backward:before { - content: "\f048"; -} -.fa-fast-backward:before { - content: "\f049"; -} -.fa-backward:before { - content: "\f04a"; -} -.fa-play:before { - content: "\f04b"; -} -.fa-pause:before { - content: "\f04c"; -} -.fa-stop:before { - content: "\f04d"; -} -.fa-forward:before { - content: "\f04e"; -} -.fa-fast-forward:before { - content: "\f050"; -} -.fa-step-forward:before { - content: "\f051"; -} -.fa-eject:before { - content: "\f052"; -} -.fa-chevron-left:before { - content: "\f053"; -} -.fa-chevron-right:before { - content: "\f054"; -} -.fa-plus-circle:before { - content: "\f055"; -} -.fa-minus-circle:before { - content: "\f056"; -} -.fa-times-circle:before { - content: "\f057"; -} -.fa-check-circle:before { - content: "\f058"; -} -.fa-question-circle:before { - content: "\f059"; -} -.fa-info-circle:before { - content: "\f05a"; -} -.fa-crosshairs:before { - content: "\f05b"; -} -.fa-times-circle-o:before { - content: "\f05c"; -} -.fa-check-circle-o:before { - content: "\f05d"; -} -.fa-ban:before { - content: "\f05e"; -} -.fa-arrow-left:before { - content: "\f060"; -} -.fa-arrow-right:before { - content: "\f061"; -} -.fa-arrow-up:before { - content: "\f062"; -} -.fa-arrow-down:before { - content: "\f063"; -} -.fa-mail-forward:before, -.fa-share:before { - content: "\f064"; -} -.fa-expand:before { - content: "\f065"; -} -.fa-compress:before { - content: "\f066"; -} -.fa-plus:before { - content: "\f067"; -} -.fa-minus:before { - content: "\f068"; -} -.fa-asterisk:before { - content: "\f069"; -} -.fa-exclamation-circle:before { - content: "\f06a"; -} -.fa-gift:before { - content: "\f06b"; -} -.fa-leaf:before { - content: "\f06c"; -} -.fa-fire:before { - content: "\f06d"; -} -.fa-eye:before { - content: "\f06e"; -} -.fa-eye-slash:before { - content: "\f070"; -} -.fa-warning:before, -.fa-exclamation-triangle:before { - content: "\f071"; -} -.fa-plane:before { - content: "\f072"; -} -.fa-calendar:before { - content: "\f073"; -} -.fa-random:before { - content: "\f074"; -} -.fa-comment:before { - content: "\f075"; -} -.fa-magnet:before { - content: "\f076"; -} -.fa-chevron-up:before { - content: "\f077"; -} -.fa-chevron-down:before { - content: "\f078"; -} -.fa-retweet:before { - content: "\f079"; -} -.fa-shopping-cart:before { - content: "\f07a"; -} -.fa-folder:before { - content: "\f07b"; -} -.fa-folder-open:before { - content: "\f07c"; -} -.fa-arrows-v:before { - content: "\f07d"; -} -.fa-arrows-h:before { - content: "\f07e"; -} -.fa-bar-chart-o:before, -.fa-bar-chart:before { - content: "\f080"; -} -.fa-twitter-square:before { - content: "\f081"; -} -.fa-facebook-square:before { - content: "\f082"; -} -.fa-camera-retro:before { - content: "\f083"; -} -.fa-key:before { - content: "\f084"; -} -.fa-gears:before, -.fa-cogs:before { - content: "\f085"; -} -.fa-comments:before { - content: "\f086"; -} -.fa-thumbs-o-up:before { - content: "\f087"; -} -.fa-thumbs-o-down:before { - content: "\f088"; -} -.fa-star-half:before { - content: "\f089"; -} -.fa-heart-o:before { - content: "\f08a"; -} -.fa-sign-out:before { - content: "\f08b"; -} -.fa-linkedin-square:before { - content: "\f08c"; -} -.fa-thumb-tack:before { - content: "\f08d"; -} -.fa-external-link:before { - content: "\f08e"; -} -.fa-sign-in:before { - content: "\f090"; -} -.fa-trophy:before { - content: "\f091"; -} -.fa-github-square:before { - content: "\f092"; -} -.fa-upload:before { - content: "\f093"; -} -.fa-lemon-o:before { - content: "\f094"; -} -.fa-phone:before { - content: "\f095"; -} -.fa-square-o:before { - content: "\f096"; -} -.fa-bookmark-o:before { - content: "\f097"; -} -.fa-phone-square:before { - content: "\f098"; -} -.fa-twitter:before { - content: "\f099"; -} -.fa-facebook-f:before, -.fa-facebook:before { - content: "\f09a"; -} -.fa-github:before { - content: "\f09b"; -} -.fa-unlock:before { - content: "\f09c"; -} -.fa-credit-card:before { - content: "\f09d"; -} -.fa-feed:before, -.fa-rss:before { - content: "\f09e"; -} -.fa-hdd-o:before { - content: "\f0a0"; -} -.fa-bullhorn:before { - content: "\f0a1"; -} -.fa-bell:before { - content: "\f0f3"; -} -.fa-certificate:before { - content: "\f0a3"; -} -.fa-hand-o-right:before { - content: "\f0a4"; -} -.fa-hand-o-left:before { - content: "\f0a5"; -} -.fa-hand-o-up:before { - content: "\f0a6"; -} -.fa-hand-o-down:before { - content: "\f0a7"; -} -.fa-arrow-circle-left:before { - content: "\f0a8"; -} -.fa-arrow-circle-right:before { - content: "\f0a9"; -} -.fa-arrow-circle-up:before { - content: "\f0aa"; -} -.fa-arrow-circle-down:before { - content: "\f0ab"; -} -.fa-globe:before { - content: "\f0ac"; -} -.fa-wrench:before { - content: "\f0ad"; -} -.fa-tasks:before { - content: "\f0ae"; -} -.fa-filter:before { - content: "\f0b0"; -} -.fa-briefcase:before { - content: "\f0b1"; -} -.fa-arrows-alt:before { - content: "\f0b2"; -} -.fa-group:before, -.fa-users:before { - content: "\f0c0"; -} -.fa-chain:before, -.fa-link:before { - content: "\f0c1"; -} -.fa-cloud:before { - content: "\f0c2"; -} -.fa-flask:before { - content: "\f0c3"; -} -.fa-cut:before, -.fa-scissors:before { - content: "\f0c4"; -} -.fa-copy:before, -.fa-files-o:before { - content: "\f0c5"; -} -.fa-paperclip:before { - content: "\f0c6"; -} -.fa-save:before, -.fa-floppy-o:before { - content: "\f0c7"; -} -.fa-square:before { - content: "\f0c8"; -} -.fa-navicon:before, -.fa-reorder:before, -.fa-bars:before { - content: "\f0c9"; -} -.fa-list-ul:before { - content: "\f0ca"; -} -.fa-list-ol:before { - content: "\f0cb"; -} -.fa-strikethrough:before { - content: "\f0cc"; -} -.fa-underline:before { - content: "\f0cd"; -} -.fa-table:before { - content: "\f0ce"; -} -.fa-magic:before { - content: "\f0d0"; -} -.fa-truck:before { - content: "\f0d1"; -} -.fa-pinterest:before { - content: "\f0d2"; -} -.fa-pinterest-square:before { - content: "\f0d3"; -} -.fa-google-plus-square:before { - content: "\f0d4"; -} -.fa-google-plus:before { - content: "\f0d5"; -} -.fa-money:before { - content: "\f0d6"; -} -.fa-caret-down:before { - content: "\f0d7"; -} -.fa-caret-up:before { - content: "\f0d8"; -} -.fa-caret-left:before { - content: "\f0d9"; -} -.fa-caret-right:before { - content: "\f0da"; -} -.fa-columns:before { - content: "\f0db"; -} -.fa-unsorted:before, -.fa-sort:before { - content: "\f0dc"; -} -.fa-sort-down:before, -.fa-sort-desc:before { - content: "\f0dd"; -} -.fa-sort-up:before, -.fa-sort-asc:before { - content: "\f0de"; -} -.fa-envelope:before { - content: "\f0e0"; -} -.fa-linkedin:before { - content: "\f0e1"; -} -.fa-rotate-left:before, -.fa-undo:before { - content: "\f0e2"; -} -.fa-legal:before, -.fa-gavel:before { - content: "\f0e3"; -} -.fa-dashboard:before, -.fa-tachometer:before { - content: "\f0e4"; -} -.fa-comment-o:before { - content: "\f0e5"; -} -.fa-comments-o:before { - content: "\f0e6"; -} -.fa-flash:before, -.fa-bolt:before { - content: "\f0e7"; -} -.fa-sitemap:before { - content: "\f0e8"; -} -.fa-umbrella:before { - content: "\f0e9"; -} -.fa-paste:before, -.fa-clipboard:before { - content: "\f0ea"; -} -.fa-lightbulb-o:before { - content: "\f0eb"; -} -.fa-exchange:before { - content: "\f0ec"; -} -.fa-cloud-download:before { - content: "\f0ed"; -} -.fa-cloud-upload:before { - content: "\f0ee"; -} -.fa-user-md:before { - content: "\f0f0"; -} -.fa-stethoscope:before { - content: "\f0f1"; -} -.fa-suitcase:before { - content: "\f0f2"; -} -.fa-bell-o:before { - content: "\f0a2"; -} -.fa-coffee:before { - content: "\f0f4"; -} -.fa-cutlery:before { - content: "\f0f5"; -} -.fa-file-text-o:before { - content: "\f0f6"; -} -.fa-building-o:before { - content: "\f0f7"; -} -.fa-hospital-o:before { - content: "\f0f8"; -} -.fa-ambulance:before { - content: "\f0f9"; -} -.fa-medkit:before { - content: "\f0fa"; -} -.fa-fighter-jet:before { - content: "\f0fb"; -} -.fa-beer:before { - content: "\f0fc"; -} -.fa-h-square:before { - content: "\f0fd"; -} -.fa-plus-square:before { - content: "\f0fe"; -} -.fa-angle-double-left:before { - content: "\f100"; -} -.fa-angle-double-right:before { - content: "\f101"; -} -.fa-angle-double-up:before { - content: "\f102"; -} -.fa-angle-double-down:before { - content: "\f103"; -} -.fa-angle-left:before { - content: "\f104"; -} -.fa-angle-right:before { - content: "\f105"; -} -.fa-angle-up:before { - content: "\f106"; -} -.fa-angle-down:before { - content: "\f107"; -} -.fa-desktop:before { - content: "\f108"; -} -.fa-laptop:before { - content: "\f109"; -} -.fa-tablet:before { - content: "\f10a"; -} -.fa-mobile-phone:before, -.fa-mobile:before { - content: "\f10b"; -} -.fa-circle-o:before { - content: "\f10c"; -} -.fa-quote-left:before { - content: "\f10d"; -} -.fa-quote-right:before { - content: "\f10e"; -} -.fa-spinner:before { - content: "\f110"; -} -.fa-circle:before { - content: "\f111"; -} -.fa-mail-reply:before, -.fa-reply:before { - content: "\f112"; -} -.fa-github-alt:before { - content: "\f113"; -} -.fa-folder-o:before { - content: "\f114"; -} -.fa-folder-open-o:before { - content: "\f115"; -} -.fa-smile-o:before { - content: "\f118"; -} -.fa-frown-o:before { - content: "\f119"; -} -.fa-meh-o:before { - content: "\f11a"; -} -.fa-gamepad:before { - content: "\f11b"; -} -.fa-keyboard-o:before { - content: "\f11c"; -} -.fa-flag-o:before { - content: "\f11d"; -} -.fa-flag-checkered:before { - content: "\f11e"; -} -.fa-terminal:before { - content: "\f120"; -} -.fa-code:before { - content: "\f121"; -} -.fa-mail-reply-all:before, -.fa-reply-all:before { - content: "\f122"; -} -.fa-star-half-empty:before, -.fa-star-half-full:before, -.fa-star-half-o:before { - content: "\f123"; -} -.fa-location-arrow:before { - content: "\f124"; -} -.fa-crop:before { - content: "\f125"; -} -.fa-code-fork:before { - content: "\f126"; -} -.fa-unlink:before, -.fa-chain-broken:before { - content: "\f127"; -} -.fa-question:before { - content: "\f128"; -} -.fa-info:before { - content: "\f129"; -} -.fa-exclamation:before { - content: "\f12a"; -} -.fa-superscript:before { - content: "\f12b"; -} -.fa-subscript:before { - content: "\f12c"; -} -.fa-eraser:before { - content: "\f12d"; -} -.fa-puzzle-piece:before { - content: "\f12e"; -} -.fa-microphone:before { - content: "\f130"; -} -.fa-microphone-slash:before { - content: "\f131"; -} -.fa-shield:before { - content: "\f132"; -} -.fa-calendar-o:before { - content: "\f133"; -} -.fa-fire-extinguisher:before { - content: "\f134"; -} -.fa-rocket:before { - content: "\f135"; -} -.fa-maxcdn:before { - content: "\f136"; -} -.fa-chevron-circle-left:before { - content: "\f137"; -} -.fa-chevron-circle-right:before { - content: "\f138"; -} -.fa-chevron-circle-up:before { - content: "\f139"; -} -.fa-chevron-circle-down:before { - content: "\f13a"; -} -.fa-html5:before { - content: "\f13b"; -} -.fa-css3:before { - content: "\f13c"; -} -.fa-anchor:before { - content: "\f13d"; -} -.fa-unlock-alt:before { - content: "\f13e"; -} -.fa-bullseye:before { - content: "\f140"; -} -.fa-ellipsis-h:before { - content: "\f141"; -} -.fa-ellipsis-v:before { - content: "\f142"; -} -.fa-rss-square:before { - content: "\f143"; -} -.fa-play-circle:before { - content: "\f144"; -} -.fa-ticket:before { - content: "\f145"; -} -.fa-minus-square:before { - content: "\f146"; -} -.fa-minus-square-o:before { - content: "\f147"; -} -.fa-level-up:before { - content: "\f148"; -} -.fa-level-down:before { - content: "\f149"; -} -.fa-check-square:before { - content: "\f14a"; -} -.fa-pencil-square:before { - content: "\f14b"; -} -.fa-external-link-square:before { - content: "\f14c"; -} -.fa-share-square:before { - content: "\f14d"; -} -.fa-compass:before { - content: "\f14e"; -} -.fa-toggle-down:before, -.fa-caret-square-o-down:before { - content: "\f150"; -} -.fa-toggle-up:before, -.fa-caret-square-o-up:before { - content: "\f151"; -} -.fa-toggle-right:before, -.fa-caret-square-o-right:before { - content: "\f152"; -} -.fa-euro:before, -.fa-eur:before { - content: "\f153"; -} -.fa-gbp:before { - content: "\f154"; -} -.fa-dollar:before, -.fa-usd:before { - content: "\f155"; -} -.fa-rupee:before, -.fa-inr:before { - content: "\f156"; -} -.fa-cny:before, -.fa-rmb:before, -.fa-yen:before, -.fa-jpy:before { - content: "\f157"; -} -.fa-ruble:before, -.fa-rouble:before, -.fa-rub:before { - content: "\f158"; -} -.fa-won:before, -.fa-krw:before { - content: "\f159"; -} -.fa-bitcoin:before, -.fa-btc:before { - content: "\f15a"; -} -.fa-file:before { - content: "\f15b"; -} -.fa-file-text:before { - content: "\f15c"; -} -.fa-sort-alpha-asc:before { - content: "\f15d"; -} -.fa-sort-alpha-desc:before { - content: "\f15e"; -} -.fa-sort-amount-asc:before { - content: "\f160"; -} -.fa-sort-amount-desc:before { - content: "\f161"; -} -.fa-sort-numeric-asc:before { - content: "\f162"; -} -.fa-sort-numeric-desc:before { - content: "\f163"; -} -.fa-thumbs-up:before { - content: "\f164"; -} -.fa-thumbs-down:before { - content: "\f165"; -} -.fa-youtube-square:before { - content: "\f166"; -} -.fa-youtube:before { - content: "\f167"; -} -.fa-xing:before { - content: "\f168"; -} -.fa-xing-square:before { - content: "\f169"; -} -.fa-youtube-play:before { - content: "\f16a"; -} -.fa-dropbox:before { - content: "\f16b"; -} -.fa-stack-overflow:before { - content: "\f16c"; -} -.fa-instagram:before { - content: "\f16d"; -} -.fa-flickr:before { - content: "\f16e"; -} -.fa-adn:before { - content: "\f170"; -} -.fa-bitbucket:before { - content: "\f171"; -} -.fa-bitbucket-square:before { - content: "\f172"; -} -.fa-tumblr:before { - content: "\f173"; -} -.fa-tumblr-square:before { - content: "\f174"; -} -.fa-long-arrow-down:before { - content: "\f175"; -} -.fa-long-arrow-up:before { - content: "\f176"; -} -.fa-long-arrow-left:before { - content: "\f177"; -} -.fa-long-arrow-right:before { - content: "\f178"; -} -.fa-apple:before { - content: "\f179"; -} -.fa-windows:before { - content: "\f17a"; -} -.fa-android:before { - content: "\f17b"; -} -.fa-linux:before { - content: "\f17c"; -} -.fa-dribbble:before { - content: "\f17d"; -} -.fa-skype:before { - content: "\f17e"; -} -.fa-foursquare:before { - content: "\f180"; -} -.fa-trello:before { - content: "\f181"; -} -.fa-female:before { - content: "\f182"; -} -.fa-male:before { - content: "\f183"; -} -.fa-gittip:before, -.fa-gratipay:before { - content: "\f184"; -} -.fa-sun-o:before { - content: "\f185"; -} -.fa-moon-o:before { - content: "\f186"; -} -.fa-archive:before { - content: "\f187"; -} -.fa-bug:before { - content: "\f188"; -} -.fa-vk:before { - content: "\f189"; -} -.fa-weibo:before { - content: "\f18a"; -} -.fa-renren:before { - content: "\f18b"; -} -.fa-pagelines:before { - content: "\f18c"; -} -.fa-stack-exchange:before { - content: "\f18d"; -} -.fa-arrow-circle-o-right:before { - content: "\f18e"; -} -.fa-arrow-circle-o-left:before { - content: "\f190"; -} -.fa-toggle-left:before, -.fa-caret-square-o-left:before { - content: "\f191"; -} -.fa-dot-circle-o:before { - content: "\f192"; -} -.fa-wheelchair:before { - content: "\f193"; -} -.fa-vimeo-square:before { - content: "\f194"; -} -.fa-turkish-lira:before, -.fa-try:before { - content: "\f195"; -} -.fa-plus-square-o:before { - content: "\f196"; -} -.fa-space-shuttle:before { - content: "\f197"; -} -.fa-slack:before { - content: "\f198"; -} -.fa-envelope-square:before { - content: "\f199"; -} -.fa-wordpress:before { - content: "\f19a"; -} -.fa-openid:before { - content: "\f19b"; -} -.fa-institution:before, -.fa-bank:before, -.fa-university:before { - content: "\f19c"; -} -.fa-mortar-board:before, -.fa-graduation-cap:before { - content: "\f19d"; -} -.fa-yahoo:before { - content: "\f19e"; -} -.fa-google:before { - content: "\f1a0"; -} -.fa-reddit:before { - content: "\f1a1"; -} -.fa-reddit-square:before { - content: "\f1a2"; -} -.fa-stumbleupon-circle:before { - content: "\f1a3"; -} -.fa-stumbleupon:before { - content: "\f1a4"; -} -.fa-delicious:before { - content: "\f1a5"; -} -.fa-digg:before { - content: "\f1a6"; -} -.fa-pied-piper-pp:before { - content: "\f1a7"; -} -.fa-pied-piper-alt:before { - content: "\f1a8"; -} -.fa-drupal:before { - content: "\f1a9"; -} -.fa-joomla:before { - content: "\f1aa"; -} -.fa-language:before { - content: "\f1ab"; -} -.fa-fax:before { - content: "\f1ac"; -} -.fa-building:before { - content: "\f1ad"; -} -.fa-child:before { - content: "\f1ae"; -} -.fa-paw:before { - content: "\f1b0"; -} -.fa-spoon:before { - content: "\f1b1"; -} -.fa-cube:before { - content: "\f1b2"; -} -.fa-cubes:before { - content: "\f1b3"; -} -.fa-behance:before { - content: "\f1b4"; -} -.fa-behance-square:before { - content: "\f1b5"; -} -.fa-steam:before { - content: "\f1b6"; -} -.fa-steam-square:before { - content: "\f1b7"; -} -.fa-recycle:before { - content: "\f1b8"; -} -.fa-automobile:before, -.fa-car:before { - content: "\f1b9"; -} -.fa-cab:before, -.fa-taxi:before { - content: "\f1ba"; -} -.fa-tree:before { - content: "\f1bb"; -} -.fa-spotify:before { - content: "\f1bc"; -} -.fa-deviantart:before { - content: "\f1bd"; -} -.fa-soundcloud:before { - content: "\f1be"; -} -.fa-database:before { - content: "\f1c0"; -} -.fa-file-pdf-o:before { - content: "\f1c1"; -} -.fa-file-word-o:before { - content: "\f1c2"; -} -.fa-file-excel-o:before { - content: "\f1c3"; -} -.fa-file-powerpoint-o:before { - content: "\f1c4"; -} -.fa-file-photo-o:before, -.fa-file-picture-o:before, -.fa-file-image-o:before { - content: "\f1c5"; -} -.fa-file-zip-o:before, -.fa-file-archive-o:before { - content: "\f1c6"; -} -.fa-file-sound-o:before, -.fa-file-audio-o:before { - content: "\f1c7"; -} -.fa-file-movie-o:before, -.fa-file-video-o:before { - content: "\f1c8"; -} -.fa-file-code-o:before { - content: "\f1c9"; -} -.fa-vine:before { - content: "\f1ca"; -} -.fa-codepen:before { - content: "\f1cb"; -} -.fa-jsfiddle:before { - content: "\f1cc"; -} -.fa-life-bouy:before, -.fa-life-buoy:before, -.fa-life-saver:before, -.fa-support:before, -.fa-life-ring:before { - content: "\f1cd"; -} -.fa-circle-o-notch:before { - content: "\f1ce"; -} -.fa-ra:before, -.fa-resistance:before, -.fa-rebel:before { - content: "\f1d0"; -} -.fa-ge:before, -.fa-empire:before { - content: "\f1d1"; -} -.fa-git-square:before { - content: "\f1d2"; -} -.fa-git:before { - content: "\f1d3"; -} -.fa-y-combinator-square:before, -.fa-yc-square:before, -.fa-hacker-news:before { - content: "\f1d4"; -} -.fa-tencent-weibo:before { - content: "\f1d5"; -} -.fa-qq:before { - content: "\f1d6"; -} -.fa-wechat:before, -.fa-weixin:before { - content: "\f1d7"; -} -.fa-send:before, -.fa-paper-plane:before { - content: "\f1d8"; -} -.fa-send-o:before, -.fa-paper-plane-o:before { - content: "\f1d9"; -} -.fa-history:before { - content: "\f1da"; -} -.fa-circle-thin:before { - content: "\f1db"; -} -.fa-header:before { - content: "\f1dc"; -} -.fa-paragraph:before { - content: "\f1dd"; -} -.fa-sliders:before { - content: "\f1de"; -} -.fa-share-alt:before { - content: "\f1e0"; -} -.fa-share-alt-square:before { - content: "\f1e1"; -} -.fa-bomb:before { - content: "\f1e2"; -} -.fa-soccer-ball-o:before, -.fa-futbol-o:before { - content: "\f1e3"; -} -.fa-tty:before { - content: "\f1e4"; -} -.fa-binoculars:before { - content: "\f1e5"; -} -.fa-plug:before { - content: "\f1e6"; -} -.fa-slideshare:before { - content: "\f1e7"; -} -.fa-twitch:before { - content: "\f1e8"; -} -.fa-yelp:before { - content: "\f1e9"; -} -.fa-newspaper-o:before { - content: "\f1ea"; -} -.fa-wifi:before { - content: "\f1eb"; -} -.fa-calculator:before { - content: "\f1ec"; -} -.fa-paypal:before { - content: "\f1ed"; -} -.fa-google-wallet:before { - content: "\f1ee"; -} -.fa-cc-visa:before { - content: "\f1f0"; -} -.fa-cc-mastercard:before { - content: "\f1f1"; -} -.fa-cc-discover:before { - content: "\f1f2"; -} -.fa-cc-amex:before { - content: "\f1f3"; -} -.fa-cc-paypal:before { - content: "\f1f4"; -} -.fa-cc-stripe:before { - content: "\f1f5"; -} -.fa-bell-slash:before { - content: "\f1f6"; -} -.fa-bell-slash-o:before { - content: "\f1f7"; -} -.fa-trash:before { - content: "\f1f8"; -} -.fa-copyright:before { - content: "\f1f9"; -} -.fa-at:before { - content: "\f1fa"; -} -.fa-eyedropper:before { - content: "\f1fb"; -} -.fa-paint-brush:before { - content: "\f1fc"; -} -.fa-birthday-cake:before { - content: "\f1fd"; -} -.fa-area-chart:before { - content: "\f1fe"; -} -.fa-pie-chart:before { - content: "\f200"; -} -.fa-line-chart:before { - content: "\f201"; -} -.fa-lastfm:before { - content: "\f202"; -} -.fa-lastfm-square:before { - content: "\f203"; -} -.fa-toggle-off:before { - content: "\f204"; -} -.fa-toggle-on:before { - content: "\f205"; -} -.fa-bicycle:before { - content: "\f206"; -} -.fa-bus:before { - content: "\f207"; -} -.fa-ioxhost:before { - content: "\f208"; -} -.fa-angellist:before { - content: "\f209"; -} -.fa-cc:before { - content: "\f20a"; -} -.fa-shekel:before, -.fa-sheqel:before, -.fa-ils:before { - content: "\f20b"; -} -.fa-meanpath:before { - content: "\f20c"; -} -.fa-buysellads:before { - content: "\f20d"; -} -.fa-connectdevelop:before { - content: "\f20e"; -} -.fa-dashcube:before { - content: "\f210"; -} -.fa-forumbee:before { - content: "\f211"; -} -.fa-leanpub:before { - content: "\f212"; -} -.fa-sellsy:before { - content: "\f213"; -} -.fa-shirtsinbulk:before { - content: "\f214"; -} -.fa-simplybuilt:before { - content: "\f215"; -} -.fa-skyatlas:before { - content: "\f216"; -} -.fa-cart-plus:before { - content: "\f217"; -} -.fa-cart-arrow-down:before { - content: "\f218"; -} -.fa-diamond:before { - content: "\f219"; -} -.fa-ship:before { - content: "\f21a"; -} -.fa-user-secret:before { - content: "\f21b"; -} -.fa-motorcycle:before { - content: "\f21c"; -} -.fa-street-view:before { - content: "\f21d"; -} -.fa-heartbeat:before { - content: "\f21e"; -} -.fa-venus:before { - content: "\f221"; -} -.fa-mars:before { - content: "\f222"; -} -.fa-mercury:before { - content: "\f223"; -} -.fa-intersex:before, -.fa-transgender:before { - content: "\f224"; -} -.fa-transgender-alt:before { - content: "\f225"; -} -.fa-venus-double:before { - content: "\f226"; -} -.fa-mars-double:before { - content: "\f227"; -} -.fa-venus-mars:before { - content: "\f228"; -} -.fa-mars-stroke:before { - content: "\f229"; -} -.fa-mars-stroke-v:before { - content: "\f22a"; -} -.fa-mars-stroke-h:before { - content: "\f22b"; -} -.fa-neuter:before { - content: "\f22c"; -} -.fa-genderless:before { - content: "\f22d"; -} -.fa-facebook-official:before { - content: "\f230"; -} -.fa-pinterest-p:before { - content: "\f231"; -} -.fa-whatsapp:before { - content: "\f232"; -} -.fa-server:before { - content: "\f233"; -} -.fa-user-plus:before { - content: "\f234"; -} -.fa-user-times:before { - content: "\f235"; -} -.fa-hotel:before, -.fa-bed:before { - content: "\f236"; -} -.fa-viacoin:before { - content: "\f237"; -} -.fa-train:before { - content: "\f238"; -} -.fa-subway:before { - content: "\f239"; -} -.fa-medium:before { - content: "\f23a"; -} -.fa-yc:before, -.fa-y-combinator:before { - content: "\f23b"; -} -.fa-optin-monster:before { - content: "\f23c"; -} -.fa-opencart:before { - content: "\f23d"; -} -.fa-expeditedssl:before { - content: "\f23e"; -} -.fa-battery-4:before, -.fa-battery:before, -.fa-battery-full:before { - content: "\f240"; -} -.fa-battery-3:before, -.fa-battery-three-quarters:before { - content: "\f241"; -} -.fa-battery-2:before, -.fa-battery-half:before { - content: "\f242"; -} -.fa-battery-1:before, -.fa-battery-quarter:before { - content: "\f243"; -} -.fa-battery-0:before, -.fa-battery-empty:before { - content: "\f244"; -} -.fa-mouse-pointer:before { - content: "\f245"; -} -.fa-i-cursor:before { - content: "\f246"; -} -.fa-object-group:before { - content: "\f247"; -} -.fa-object-ungroup:before { - content: "\f248"; -} -.fa-sticky-note:before { - content: "\f249"; -} -.fa-sticky-note-o:before { - content: "\f24a"; -} -.fa-cc-jcb:before { - content: "\f24b"; -} -.fa-cc-diners-club:before { - content: "\f24c"; -} -.fa-clone:before { - content: "\f24d"; -} -.fa-balance-scale:before { - content: "\f24e"; -} -.fa-hourglass-o:before { - content: "\f250"; -} -.fa-hourglass-1:before, -.fa-hourglass-start:before { - content: "\f251"; -} -.fa-hourglass-2:before, -.fa-hourglass-half:before { - content: "\f252"; -} -.fa-hourglass-3:before, -.fa-hourglass-end:before { - content: "\f253"; -} -.fa-hourglass:before { - content: "\f254"; -} -.fa-hand-grab-o:before, -.fa-hand-rock-o:before { - content: "\f255"; -} -.fa-hand-stop-o:before, -.fa-hand-paper-o:before { - content: "\f256"; -} -.fa-hand-scissors-o:before { - content: "\f257"; -} -.fa-hand-lizard-o:before { - content: "\f258"; -} -.fa-hand-spock-o:before { - content: "\f259"; -} -.fa-hand-pointer-o:before { - content: "\f25a"; -} -.fa-hand-peace-o:before { - content: "\f25b"; -} -.fa-trademark:before { - content: "\f25c"; -} -.fa-registered:before { - content: "\f25d"; -} -.fa-creative-commons:before { - content: "\f25e"; -} -.fa-gg:before { - content: "\f260"; -} -.fa-gg-circle:before { - content: "\f261"; -} -.fa-tripadvisor:before { - content: "\f262"; -} -.fa-odnoklassniki:before { - content: "\f263"; -} -.fa-odnoklassniki-square:before { - content: "\f264"; -} -.fa-get-pocket:before { - content: "\f265"; -} -.fa-wikipedia-w:before { - content: "\f266"; -} -.fa-safari:before { - content: "\f267"; -} -.fa-chrome:before { - content: "\f268"; -} -.fa-firefox:before { - content: "\f269"; -} -.fa-opera:before { - content: "\f26a"; -} -.fa-internet-explorer:before { - content: "\f26b"; -} -.fa-tv:before, -.fa-television:before { - content: "\f26c"; -} -.fa-contao:before { - content: "\f26d"; -} -.fa-500px:before { - content: "\f26e"; -} -.fa-amazon:before { - content: "\f270"; -} -.fa-calendar-plus-o:before { - content: "\f271"; -} -.fa-calendar-minus-o:before { - content: "\f272"; -} -.fa-calendar-times-o:before { - content: "\f273"; -} -.fa-calendar-check-o:before { - content: "\f274"; -} -.fa-industry:before { - content: "\f275"; -} -.fa-map-pin:before { - content: "\f276"; -} -.fa-map-signs:before { - content: "\f277"; -} -.fa-map-o:before { - content: "\f278"; -} -.fa-map:before { - content: "\f279"; -} -.fa-commenting:before { - content: "\f27a"; -} -.fa-commenting-o:before { - content: "\f27b"; -} -.fa-houzz:before { - content: "\f27c"; -} -.fa-vimeo:before { - content: "\f27d"; -} -.fa-black-tie:before { - content: "\f27e"; -} -.fa-fonticons:before { - content: "\f280"; -} -.fa-reddit-alien:before { - content: "\f281"; -} -.fa-edge:before { - content: "\f282"; -} -.fa-credit-card-alt:before { - content: "\f283"; -} -.fa-codiepie:before { - content: "\f284"; -} -.fa-modx:before { - content: "\f285"; -} -.fa-fort-awesome:before { - content: "\f286"; -} -.fa-usb:before { - content: "\f287"; -} -.fa-product-hunt:before { - content: "\f288"; -} -.fa-mixcloud:before { - content: "\f289"; -} -.fa-scribd:before { - content: "\f28a"; -} -.fa-pause-circle:before { - content: "\f28b"; -} -.fa-pause-circle-o:before { - content: "\f28c"; -} -.fa-stop-circle:before { - content: "\f28d"; -} -.fa-stop-circle-o:before { - content: "\f28e"; -} -.fa-shopping-bag:before { - content: "\f290"; -} -.fa-shopping-basket:before { - content: "\f291"; -} -.fa-hashtag:before { - content: "\f292"; -} -.fa-bluetooth:before { - content: "\f293"; -} -.fa-bluetooth-b:before { - content: "\f294"; -} -.fa-percent:before { - content: "\f295"; -} -.fa-gitlab:before { - content: "\f296"; -} -.fa-wpbeginner:before { - content: "\f297"; -} -.fa-wpforms:before { - content: "\f298"; -} -.fa-envira:before { - content: "\f299"; -} -.fa-universal-access:before { - content: "\f29a"; -} -.fa-wheelchair-alt:before { - content: "\f29b"; -} -.fa-question-circle-o:before { - content: "\f29c"; -} -.fa-blind:before { - content: "\f29d"; -} -.fa-audio-description:before { - content: "\f29e"; -} -.fa-volume-control-phone:before { - content: "\f2a0"; -} -.fa-braille:before { - content: "\f2a1"; -} -.fa-assistive-listening-systems:before { - content: "\f2a2"; -} -.fa-asl-interpreting:before, -.fa-american-sign-language-interpreting:before { - content: "\f2a3"; -} -.fa-deafness:before, -.fa-hard-of-hearing:before, -.fa-deaf:before { - content: "\f2a4"; -} -.fa-glide:before { - content: "\f2a5"; -} -.fa-glide-g:before { - content: "\f2a6"; -} -.fa-signing:before, -.fa-sign-language:before { - content: "\f2a7"; -} -.fa-low-vision:before { - content: "\f2a8"; -} -.fa-viadeo:before { - content: "\f2a9"; -} -.fa-viadeo-square:before { - content: "\f2aa"; -} -.fa-snapchat:before { - content: "\f2ab"; -} -.fa-snapchat-ghost:before { - content: "\f2ac"; -} -.fa-snapchat-square:before { - content: "\f2ad"; -} -.fa-pied-piper:before { - content: "\f2ae"; -} -.fa-first-order:before { - content: "\f2b0"; -} -.fa-yoast:before { - content: "\f2b1"; -} -.fa-themeisle:before { - content: "\f2b2"; -} -.fa-google-plus-circle:before, -.fa-google-plus-official:before { - content: "\f2b3"; -} -.fa-fa:before, -.fa-font-awesome:before { - content: "\f2b4"; -} -.fa-handshake-o:before { - content: "\f2b5"; -} -.fa-envelope-open:before { - content: "\f2b6"; -} -.fa-envelope-open-o:before { - content: "\f2b7"; -} -.fa-linode:before { - content: "\f2b8"; -} -.fa-address-book:before { - content: "\f2b9"; -} -.fa-address-book-o:before { - content: "\f2ba"; -} -.fa-vcard:before, -.fa-address-card:before { - content: "\f2bb"; -} -.fa-vcard-o:before, -.fa-address-card-o:before { - content: "\f2bc"; -} -.fa-user-circle:before { - content: "\f2bd"; -} -.fa-user-circle-o:before { - content: "\f2be"; -} -.fa-user-o:before { - content: "\f2c0"; -} -.fa-id-badge:before { - content: "\f2c1"; -} -.fa-drivers-license:before, -.fa-id-card:before { - content: "\f2c2"; -} -.fa-drivers-license-o:before, -.fa-id-card-o:before { - content: "\f2c3"; -} -.fa-quora:before { - content: "\f2c4"; -} -.fa-free-code-camp:before { - content: "\f2c5"; -} -.fa-telegram:before { - content: "\f2c6"; -} -.fa-thermometer-4:before, -.fa-thermometer:before, -.fa-thermometer-full:before { - content: "\f2c7"; -} -.fa-thermometer-3:before, -.fa-thermometer-three-quarters:before { - content: "\f2c8"; -} -.fa-thermometer-2:before, -.fa-thermometer-half:before { - content: "\f2c9"; -} -.fa-thermometer-1:before, -.fa-thermometer-quarter:before { - content: "\f2ca"; -} -.fa-thermometer-0:before, -.fa-thermometer-empty:before { - content: "\f2cb"; -} -.fa-shower:before { - content: "\f2cc"; -} -.fa-bathtub:before, -.fa-s15:before, -.fa-bath:before { - content: "\f2cd"; -} -.fa-podcast:before { - content: "\f2ce"; -} -.fa-window-maximize:before { - content: "\f2d0"; -} -.fa-window-minimize:before { - content: "\f2d1"; -} -.fa-window-restore:before { - content: "\f2d2"; -} -.fa-times-rectangle:before, -.fa-window-close:before { - content: "\f2d3"; -} -.fa-times-rectangle-o:before, -.fa-window-close-o:before { - content: "\f2d4"; -} -.fa-bandcamp:before { - content: "\f2d5"; -} -.fa-grav:before { - content: "\f2d6"; -} -.fa-etsy:before { - content: "\f2d7"; -} -.fa-imdb:before { - content: "\f2d8"; -} -.fa-ravelry:before { - content: "\f2d9"; -} -.fa-eercast:before { - content: "\f2da"; -} -.fa-microchip:before { - content: "\f2db"; -} -.fa-snowflake-o:before { - content: "\f2dc"; -} -.fa-superpowers:before { - content: "\f2dd"; -} -.fa-wpexplorer:before { - content: "\f2de"; -} -.fa-meetup:before { - content: "\f2e0"; -} -.sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0, 0, 0, 0); - border: 0; -} -.sr-only-focusable:active, -.sr-only-focusable:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; -} diff --git a/resources/public/font-awesome-4.7.0/css/font-awesome.min.css b/resources/public/font-awesome-4.7.0/css/font-awesome.min.css deleted file mode 100644 index 540440ce8..000000000 --- a/resources/public/font-awesome-4.7.0/css/font-awesome.min.css +++ /dev/null @@ -1,4 +0,0 @@ -/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.7.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.7.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff2?v=4.7.0') format('woff2'),url('../fonts/fontawesome-webfont.woff?v=4.7.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.7.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.7.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.fa-pull-left{float:left}.fa-pull-right{float:right}.fa.fa-pull-left{margin-right:.3em}.fa.fa-pull-right{margin-left:.3em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}.fa-pulse{-webkit-animation:fa-spin 1s infinite steps(8);animation:fa-spin 1s infinite steps(8)}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=1)";-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2)";-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=3)";-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1)";-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{-ms-filter:"progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1)";-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook-f:before,.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-feed:before,.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before,.fa-gratipay:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper-pp:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-resistance:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-y-combinator-square:before,.fa-yc-square:before,.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"}.fa-buysellads:before{content:"\f20d"}.fa-connectdevelop:before{content:"\f20e"}.fa-dashcube:before{content:"\f210"}.fa-forumbee:before{content:"\f211"}.fa-leanpub:before{content:"\f212"}.fa-sellsy:before{content:"\f213"}.fa-shirtsinbulk:before{content:"\f214"}.fa-simplybuilt:before{content:"\f215"}.fa-skyatlas:before{content:"\f216"}.fa-cart-plus:before{content:"\f217"}.fa-cart-arrow-down:before{content:"\f218"}.fa-diamond:before{content:"\f219"}.fa-ship:before{content:"\f21a"}.fa-user-secret:before{content:"\f21b"}.fa-motorcycle:before{content:"\f21c"}.fa-street-view:before{content:"\f21d"}.fa-heartbeat:before{content:"\f21e"}.fa-venus:before{content:"\f221"}.fa-mars:before{content:"\f222"}.fa-mercury:before{content:"\f223"}.fa-intersex:before,.fa-transgender:before{content:"\f224"}.fa-transgender-alt:before{content:"\f225"}.fa-venus-double:before{content:"\f226"}.fa-mars-double:before{content:"\f227"}.fa-venus-mars:before{content:"\f228"}.fa-mars-stroke:before{content:"\f229"}.fa-mars-stroke-v:before{content:"\f22a"}.fa-mars-stroke-h:before{content:"\f22b"}.fa-neuter:before{content:"\f22c"}.fa-genderless:before{content:"\f22d"}.fa-facebook-official:before{content:"\f230"}.fa-pinterest-p:before{content:"\f231"}.fa-whatsapp:before{content:"\f232"}.fa-server:before{content:"\f233"}.fa-user-plus:before{content:"\f234"}.fa-user-times:before{content:"\f235"}.fa-hotel:before,.fa-bed:before{content:"\f236"}.fa-viacoin:before{content:"\f237"}.fa-train:before{content:"\f238"}.fa-subway:before{content:"\f239"}.fa-medium:before{content:"\f23a"}.fa-yc:before,.fa-y-combinator:before{content:"\f23b"}.fa-optin-monster:before{content:"\f23c"}.fa-opencart:before{content:"\f23d"}.fa-expeditedssl:before{content:"\f23e"}.fa-battery-4:before,.fa-battery:before,.fa-battery-full:before{content:"\f240"}.fa-battery-3:before,.fa-battery-three-quarters:before{content:"\f241"}.fa-battery-2:before,.fa-battery-half:before{content:"\f242"}.fa-battery-1:before,.fa-battery-quarter:before{content:"\f243"}.fa-battery-0:before,.fa-battery-empty:before{content:"\f244"}.fa-mouse-pointer:before{content:"\f245"}.fa-i-cursor:before{content:"\f246"}.fa-object-group:before{content:"\f247"}.fa-object-ungroup:before{content:"\f248"}.fa-sticky-note:before{content:"\f249"}.fa-sticky-note-o:before{content:"\f24a"}.fa-cc-jcb:before{content:"\f24b"}.fa-cc-diners-club:before{content:"\f24c"}.fa-clone:before{content:"\f24d"}.fa-balance-scale:before{content:"\f24e"}.fa-hourglass-o:before{content:"\f250"}.fa-hourglass-1:before,.fa-hourglass-start:before{content:"\f251"}.fa-hourglass-2:before,.fa-hourglass-half:before{content:"\f252"}.fa-hourglass-3:before,.fa-hourglass-end:before{content:"\f253"}.fa-hourglass:before{content:"\f254"}.fa-hand-grab-o:before,.fa-hand-rock-o:before{content:"\f255"}.fa-hand-stop-o:before,.fa-hand-paper-o:before{content:"\f256"}.fa-hand-scissors-o:before{content:"\f257"}.fa-hand-lizard-o:before{content:"\f258"}.fa-hand-spock-o:before{content:"\f259"}.fa-hand-pointer-o:before{content:"\f25a"}.fa-hand-peace-o:before{content:"\f25b"}.fa-trademark:before{content:"\f25c"}.fa-registered:before{content:"\f25d"}.fa-creative-commons:before{content:"\f25e"}.fa-gg:before{content:"\f260"}.fa-gg-circle:before{content:"\f261"}.fa-tripadvisor:before{content:"\f262"}.fa-odnoklassniki:before{content:"\f263"}.fa-odnoklassniki-square:before{content:"\f264"}.fa-get-pocket:before{content:"\f265"}.fa-wikipedia-w:before{content:"\f266"}.fa-safari:before{content:"\f267"}.fa-chrome:before{content:"\f268"}.fa-firefox:before{content:"\f269"}.fa-opera:before{content:"\f26a"}.fa-internet-explorer:before{content:"\f26b"}.fa-tv:before,.fa-television:before{content:"\f26c"}.fa-contao:before{content:"\f26d"}.fa-500px:before{content:"\f26e"}.fa-amazon:before{content:"\f270"}.fa-calendar-plus-o:before{content:"\f271"}.fa-calendar-minus-o:before{content:"\f272"}.fa-calendar-times-o:before{content:"\f273"}.fa-calendar-check-o:before{content:"\f274"}.fa-industry:before{content:"\f275"}.fa-map-pin:before{content:"\f276"}.fa-map-signs:before{content:"\f277"}.fa-map-o:before{content:"\f278"}.fa-map:before{content:"\f279"}.fa-commenting:before{content:"\f27a"}.fa-commenting-o:before{content:"\f27b"}.fa-houzz:before{content:"\f27c"}.fa-vimeo:before{content:"\f27d"}.fa-black-tie:before{content:"\f27e"}.fa-fonticons:before{content:"\f280"}.fa-reddit-alien:before{content:"\f281"}.fa-edge:before{content:"\f282"}.fa-credit-card-alt:before{content:"\f283"}.fa-codiepie:before{content:"\f284"}.fa-modx:before{content:"\f285"}.fa-fort-awesome:before{content:"\f286"}.fa-usb:before{content:"\f287"}.fa-product-hunt:before{content:"\f288"}.fa-mixcloud:before{content:"\f289"}.fa-scribd:before{content:"\f28a"}.fa-pause-circle:before{content:"\f28b"}.fa-pause-circle-o:before{content:"\f28c"}.fa-stop-circle:before{content:"\f28d"}.fa-stop-circle-o:before{content:"\f28e"}.fa-shopping-bag:before{content:"\f290"}.fa-shopping-basket:before{content:"\f291"}.fa-hashtag:before{content:"\f292"}.fa-bluetooth:before{content:"\f293"}.fa-bluetooth-b:before{content:"\f294"}.fa-percent:before{content:"\f295"}.fa-gitlab:before{content:"\f296"}.fa-wpbeginner:before{content:"\f297"}.fa-wpforms:before{content:"\f298"}.fa-envira:before{content:"\f299"}.fa-universal-access:before{content:"\f29a"}.fa-wheelchair-alt:before{content:"\f29b"}.fa-question-circle-o:before{content:"\f29c"}.fa-blind:before{content:"\f29d"}.fa-audio-description:before{content:"\f29e"}.fa-volume-control-phone:before{content:"\f2a0"}.fa-braille:before{content:"\f2a1"}.fa-assistive-listening-systems:before{content:"\f2a2"}.fa-asl-interpreting:before,.fa-american-sign-language-interpreting:before{content:"\f2a3"}.fa-deafness:before,.fa-hard-of-hearing:before,.fa-deaf:before{content:"\f2a4"}.fa-glide:before{content:"\f2a5"}.fa-glide-g:before{content:"\f2a6"}.fa-signing:before,.fa-sign-language:before{content:"\f2a7"}.fa-low-vision:before{content:"\f2a8"}.fa-viadeo:before{content:"\f2a9"}.fa-viadeo-square:before{content:"\f2aa"}.fa-snapchat:before{content:"\f2ab"}.fa-snapchat-ghost:before{content:"\f2ac"}.fa-snapchat-square:before{content:"\f2ad"}.fa-pied-piper:before{content:"\f2ae"}.fa-first-order:before{content:"\f2b0"}.fa-yoast:before{content:"\f2b1"}.fa-themeisle:before{content:"\f2b2"}.fa-google-plus-circle:before,.fa-google-plus-official:before{content:"\f2b3"}.fa-fa:before,.fa-font-awesome:before{content:"\f2b4"}.fa-handshake-o:before{content:"\f2b5"}.fa-envelope-open:before{content:"\f2b6"}.fa-envelope-open-o:before{content:"\f2b7"}.fa-linode:before{content:"\f2b8"}.fa-address-book:before{content:"\f2b9"}.fa-address-book-o:before{content:"\f2ba"}.fa-vcard:before,.fa-address-card:before{content:"\f2bb"}.fa-vcard-o:before,.fa-address-card-o:before{content:"\f2bc"}.fa-user-circle:before{content:"\f2bd"}.fa-user-circle-o:before{content:"\f2be"}.fa-user-o:before{content:"\f2c0"}.fa-id-badge:before{content:"\f2c1"}.fa-drivers-license:before,.fa-id-card:before{content:"\f2c2"}.fa-drivers-license-o:before,.fa-id-card-o:before{content:"\f2c3"}.fa-quora:before{content:"\f2c4"}.fa-free-code-camp:before{content:"\f2c5"}.fa-telegram:before{content:"\f2c6"}.fa-thermometer-4:before,.fa-thermometer:before,.fa-thermometer-full:before{content:"\f2c7"}.fa-thermometer-3:before,.fa-thermometer-three-quarters:before{content:"\f2c8"}.fa-thermometer-2:before,.fa-thermometer-half:before{content:"\f2c9"}.fa-thermometer-1:before,.fa-thermometer-quarter:before{content:"\f2ca"}.fa-thermometer-0:before,.fa-thermometer-empty:before{content:"\f2cb"}.fa-shower:before{content:"\f2cc"}.fa-bathtub:before,.fa-s15:before,.fa-bath:before{content:"\f2cd"}.fa-podcast:before{content:"\f2ce"}.fa-window-maximize:before{content:"\f2d0"}.fa-window-minimize:before{content:"\f2d1"}.fa-window-restore:before{content:"\f2d2"}.fa-times-rectangle:before,.fa-window-close:before{content:"\f2d3"}.fa-times-rectangle-o:before,.fa-window-close-o:before{content:"\f2d4"}.fa-bandcamp:before{content:"\f2d5"}.fa-grav:before{content:"\f2d6"}.fa-etsy:before{content:"\f2d7"}.fa-imdb:before{content:"\f2d8"}.fa-ravelry:before{content:"\f2d9"}.fa-eercast:before{content:"\f2da"}.fa-microchip:before{content:"\f2db"}.fa-snowflake-o:before{content:"\f2dc"}.fa-superpowers:before{content:"\f2dd"}.fa-wpexplorer:before{content:"\f2de"}.fa-meetup:before{content:"\f2e0"}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}.sr-only-focusable:active,.sr-only-focusable:focus{position:static;width:auto;height:auto;margin:0;overflow:visible;clip:auto} diff --git a/resources/public/font-awesome-4.7.0/fonts/FontAwesome.otf b/resources/public/font-awesome-4.7.0/fonts/FontAwesome.otf deleted file mode 100644 index 401ec0f36..000000000 Binary files a/resources/public/font-awesome-4.7.0/fonts/FontAwesome.otf and /dev/null differ diff --git a/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.eot b/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.eot deleted file mode 100644 index e9f60ca95..000000000 Binary files a/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.eot and /dev/null differ diff --git a/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.svg b/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.svg deleted file mode 100644 index 855c845e5..000000000 --- a/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.svg +++ /dev/null @@ -1,2671 +0,0 @@ - - - - -Created by FontForge 20120731 at Mon Oct 24 17:37:40 2016 - By ,,, -Copyright Dave Gandy 2016. All rights reserved. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf b/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf deleted file mode 100644 index 35acda2fa..000000000 Binary files a/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.ttf and /dev/null differ diff --git a/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.woff b/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.woff deleted file mode 100644 index 400014a4b..000000000 Binary files a/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.woff and /dev/null differ diff --git a/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2 b/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2 deleted file mode 100644 index 4d13fc604..000000000 Binary files a/resources/public/font-awesome-4.7.0/fonts/fontawesome-webfont.woff2 and /dev/null differ diff --git a/resources/public/font-awesome-4.7.0/less/animated.less b/resources/public/font-awesome-4.7.0/less/animated.less deleted file mode 100644 index 66ad52a5b..000000000 --- a/resources/public/font-awesome-4.7.0/less/animated.less +++ /dev/null @@ -1,34 +0,0 @@ -// Animated Icons -// -------------------------- - -.@{fa-css-prefix}-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; -} - -.@{fa-css-prefix}-pulse { - -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); -} - -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} - -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} diff --git a/resources/public/font-awesome-4.7.0/less/bordered-pulled.less b/resources/public/font-awesome-4.7.0/less/bordered-pulled.less deleted file mode 100644 index f1c8ad75f..000000000 --- a/resources/public/font-awesome-4.7.0/less/bordered-pulled.less +++ /dev/null @@ -1,25 +0,0 @@ -// Bordered & Pulled -// ------------------------- - -.@{fa-css-prefix}-border { - padding: .2em .25em .15em; - border: solid .08em @fa-border-color; - border-radius: .1em; -} - -.@{fa-css-prefix}-pull-left { float: left; } -.@{fa-css-prefix}-pull-right { float: right; } - -.@{fa-css-prefix} { - &.@{fa-css-prefix}-pull-left { margin-right: .3em; } - &.@{fa-css-prefix}-pull-right { margin-left: .3em; } -} - -/* Deprecated as of 4.4.0 */ -.pull-right { float: right; } -.pull-left { float: left; } - -.@{fa-css-prefix} { - &.pull-left { margin-right: .3em; } - &.pull-right { margin-left: .3em; } -} diff --git a/resources/public/font-awesome-4.7.0/less/core.less b/resources/public/font-awesome-4.7.0/less/core.less deleted file mode 100644 index c577ac84a..000000000 --- a/resources/public/font-awesome-4.7.0/less/core.less +++ /dev/null @@ -1,12 +0,0 @@ -// Base Class Definition -// ------------------------- - -.@{fa-css-prefix} { - display: inline-block; - font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration - font-size: inherit; // can't have font-size inherit on line above, so need to override - text-rendering: auto; // optimizelegibility throws things off #1094 - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -} diff --git a/resources/public/font-awesome-4.7.0/less/fixed-width.less b/resources/public/font-awesome-4.7.0/less/fixed-width.less deleted file mode 100644 index 110289f2f..000000000 --- a/resources/public/font-awesome-4.7.0/less/fixed-width.less +++ /dev/null @@ -1,6 +0,0 @@ -// Fixed Width Icons -// ------------------------- -.@{fa-css-prefix}-fw { - width: (18em / 14); - text-align: center; -} diff --git a/resources/public/font-awesome-4.7.0/less/font-awesome.less b/resources/public/font-awesome-4.7.0/less/font-awesome.less deleted file mode 100644 index c3677def3..000000000 --- a/resources/public/font-awesome-4.7.0/less/font-awesome.less +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */ - -@import "variables.less"; -@import "mixins.less"; -@import "path.less"; -@import "core.less"; -@import "larger.less"; -@import "fixed-width.less"; -@import "list.less"; -@import "bordered-pulled.less"; -@import "animated.less"; -@import "rotated-flipped.less"; -@import "stacked.less"; -@import "icons.less"; -@import "screen-reader.less"; diff --git a/resources/public/font-awesome-4.7.0/less/icons.less b/resources/public/font-awesome-4.7.0/less/icons.less deleted file mode 100644 index 159d60042..000000000 --- a/resources/public/font-awesome-4.7.0/less/icons.less +++ /dev/null @@ -1,789 +0,0 @@ -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ - -.@{fa-css-prefix}-glass:before { content: @fa-var-glass; } -.@{fa-css-prefix}-music:before { content: @fa-var-music; } -.@{fa-css-prefix}-search:before { content: @fa-var-search; } -.@{fa-css-prefix}-envelope-o:before { content: @fa-var-envelope-o; } -.@{fa-css-prefix}-heart:before { content: @fa-var-heart; } -.@{fa-css-prefix}-star:before { content: @fa-var-star; } -.@{fa-css-prefix}-star-o:before { content: @fa-var-star-o; } -.@{fa-css-prefix}-user:before { content: @fa-var-user; } -.@{fa-css-prefix}-film:before { content: @fa-var-film; } -.@{fa-css-prefix}-th-large:before { content: @fa-var-th-large; } -.@{fa-css-prefix}-th:before { content: @fa-var-th; } -.@{fa-css-prefix}-th-list:before { content: @fa-var-th-list; } -.@{fa-css-prefix}-check:before { content: @fa-var-check; } -.@{fa-css-prefix}-remove:before, -.@{fa-css-prefix}-close:before, -.@{fa-css-prefix}-times:before { content: @fa-var-times; } -.@{fa-css-prefix}-search-plus:before { content: @fa-var-search-plus; } -.@{fa-css-prefix}-search-minus:before { content: @fa-var-search-minus; } -.@{fa-css-prefix}-power-off:before { content: @fa-var-power-off; } -.@{fa-css-prefix}-signal:before { content: @fa-var-signal; } -.@{fa-css-prefix}-gear:before, -.@{fa-css-prefix}-cog:before { content: @fa-var-cog; } -.@{fa-css-prefix}-trash-o:before { content: @fa-var-trash-o; } -.@{fa-css-prefix}-home:before { content: @fa-var-home; } -.@{fa-css-prefix}-file-o:before { content: @fa-var-file-o; } -.@{fa-css-prefix}-clock-o:before { content: @fa-var-clock-o; } -.@{fa-css-prefix}-road:before { content: @fa-var-road; } -.@{fa-css-prefix}-download:before { content: @fa-var-download; } -.@{fa-css-prefix}-arrow-circle-o-down:before { content: @fa-var-arrow-circle-o-down; } -.@{fa-css-prefix}-arrow-circle-o-up:before { content: @fa-var-arrow-circle-o-up; } -.@{fa-css-prefix}-inbox:before { content: @fa-var-inbox; } -.@{fa-css-prefix}-play-circle-o:before { content: @fa-var-play-circle-o; } -.@{fa-css-prefix}-rotate-right:before, -.@{fa-css-prefix}-repeat:before { content: @fa-var-repeat; } -.@{fa-css-prefix}-refresh:before { content: @fa-var-refresh; } -.@{fa-css-prefix}-list-alt:before { content: @fa-var-list-alt; } -.@{fa-css-prefix}-lock:before { content: @fa-var-lock; } -.@{fa-css-prefix}-flag:before { content: @fa-var-flag; } -.@{fa-css-prefix}-headphones:before { content: @fa-var-headphones; } -.@{fa-css-prefix}-volume-off:before { content: @fa-var-volume-off; } -.@{fa-css-prefix}-volume-down:before { content: @fa-var-volume-down; } -.@{fa-css-prefix}-volume-up:before { content: @fa-var-volume-up; } -.@{fa-css-prefix}-qrcode:before { content: @fa-var-qrcode; } -.@{fa-css-prefix}-barcode:before { content: @fa-var-barcode; } -.@{fa-css-prefix}-tag:before { content: @fa-var-tag; } -.@{fa-css-prefix}-tags:before { content: @fa-var-tags; } -.@{fa-css-prefix}-book:before { content: @fa-var-book; } -.@{fa-css-prefix}-bookmark:before { content: @fa-var-bookmark; } -.@{fa-css-prefix}-print:before { content: @fa-var-print; } -.@{fa-css-prefix}-camera:before { content: @fa-var-camera; } -.@{fa-css-prefix}-font:before { content: @fa-var-font; } -.@{fa-css-prefix}-bold:before { content: @fa-var-bold; } -.@{fa-css-prefix}-italic:before { content: @fa-var-italic; } -.@{fa-css-prefix}-text-height:before { content: @fa-var-text-height; } -.@{fa-css-prefix}-text-width:before { content: @fa-var-text-width; } -.@{fa-css-prefix}-align-left:before { content: @fa-var-align-left; } -.@{fa-css-prefix}-align-center:before { content: @fa-var-align-center; } -.@{fa-css-prefix}-align-right:before { content: @fa-var-align-right; } -.@{fa-css-prefix}-align-justify:before { content: @fa-var-align-justify; } -.@{fa-css-prefix}-list:before { content: @fa-var-list; } -.@{fa-css-prefix}-dedent:before, -.@{fa-css-prefix}-outdent:before { content: @fa-var-outdent; } -.@{fa-css-prefix}-indent:before { content: @fa-var-indent; } -.@{fa-css-prefix}-video-camera:before { content: @fa-var-video-camera; } -.@{fa-css-prefix}-photo:before, -.@{fa-css-prefix}-image:before, -.@{fa-css-prefix}-picture-o:before { content: @fa-var-picture-o; } -.@{fa-css-prefix}-pencil:before { content: @fa-var-pencil; } -.@{fa-css-prefix}-map-marker:before { content: @fa-var-map-marker; } -.@{fa-css-prefix}-adjust:before { content: @fa-var-adjust; } -.@{fa-css-prefix}-tint:before { content: @fa-var-tint; } -.@{fa-css-prefix}-edit:before, -.@{fa-css-prefix}-pencil-square-o:before { content: @fa-var-pencil-square-o; } -.@{fa-css-prefix}-share-square-o:before { content: @fa-var-share-square-o; } -.@{fa-css-prefix}-check-square-o:before { content: @fa-var-check-square-o; } -.@{fa-css-prefix}-arrows:before { content: @fa-var-arrows; } -.@{fa-css-prefix}-step-backward:before { content: @fa-var-step-backward; } -.@{fa-css-prefix}-fast-backward:before { content: @fa-var-fast-backward; } -.@{fa-css-prefix}-backward:before { content: @fa-var-backward; } -.@{fa-css-prefix}-play:before { content: @fa-var-play; } -.@{fa-css-prefix}-pause:before { content: @fa-var-pause; } -.@{fa-css-prefix}-stop:before { content: @fa-var-stop; } -.@{fa-css-prefix}-forward:before { content: @fa-var-forward; } -.@{fa-css-prefix}-fast-forward:before { content: @fa-var-fast-forward; } -.@{fa-css-prefix}-step-forward:before { content: @fa-var-step-forward; } -.@{fa-css-prefix}-eject:before { content: @fa-var-eject; } -.@{fa-css-prefix}-chevron-left:before { content: @fa-var-chevron-left; } -.@{fa-css-prefix}-chevron-right:before { content: @fa-var-chevron-right; } -.@{fa-css-prefix}-plus-circle:before { content: @fa-var-plus-circle; } -.@{fa-css-prefix}-minus-circle:before { content: @fa-var-minus-circle; } -.@{fa-css-prefix}-times-circle:before { content: @fa-var-times-circle; } -.@{fa-css-prefix}-check-circle:before { content: @fa-var-check-circle; } -.@{fa-css-prefix}-question-circle:before { content: @fa-var-question-circle; } -.@{fa-css-prefix}-info-circle:before { content: @fa-var-info-circle; } -.@{fa-css-prefix}-crosshairs:before { content: @fa-var-crosshairs; } -.@{fa-css-prefix}-times-circle-o:before { content: @fa-var-times-circle-o; } -.@{fa-css-prefix}-check-circle-o:before { content: @fa-var-check-circle-o; } -.@{fa-css-prefix}-ban:before { content: @fa-var-ban; } -.@{fa-css-prefix}-arrow-left:before { content: @fa-var-arrow-left; } -.@{fa-css-prefix}-arrow-right:before { content: @fa-var-arrow-right; } -.@{fa-css-prefix}-arrow-up:before { content: @fa-var-arrow-up; } -.@{fa-css-prefix}-arrow-down:before { content: @fa-var-arrow-down; } -.@{fa-css-prefix}-mail-forward:before, -.@{fa-css-prefix}-share:before { content: @fa-var-share; } -.@{fa-css-prefix}-expand:before { content: @fa-var-expand; } -.@{fa-css-prefix}-compress:before { content: @fa-var-compress; } -.@{fa-css-prefix}-plus:before { content: @fa-var-plus; } -.@{fa-css-prefix}-minus:before { content: @fa-var-minus; } -.@{fa-css-prefix}-asterisk:before { content: @fa-var-asterisk; } -.@{fa-css-prefix}-exclamation-circle:before { content: @fa-var-exclamation-circle; } -.@{fa-css-prefix}-gift:before { content: @fa-var-gift; } -.@{fa-css-prefix}-leaf:before { content: @fa-var-leaf; } -.@{fa-css-prefix}-fire:before { content: @fa-var-fire; } -.@{fa-css-prefix}-eye:before { content: @fa-var-eye; } -.@{fa-css-prefix}-eye-slash:before { content: @fa-var-eye-slash; } -.@{fa-css-prefix}-warning:before, -.@{fa-css-prefix}-exclamation-triangle:before { content: @fa-var-exclamation-triangle; } -.@{fa-css-prefix}-plane:before { content: @fa-var-plane; } -.@{fa-css-prefix}-calendar:before { content: @fa-var-calendar; } -.@{fa-css-prefix}-random:before { content: @fa-var-random; } -.@{fa-css-prefix}-comment:before { content: @fa-var-comment; } -.@{fa-css-prefix}-magnet:before { content: @fa-var-magnet; } -.@{fa-css-prefix}-chevron-up:before { content: @fa-var-chevron-up; } -.@{fa-css-prefix}-chevron-down:before { content: @fa-var-chevron-down; } -.@{fa-css-prefix}-retweet:before { content: @fa-var-retweet; } -.@{fa-css-prefix}-shopping-cart:before { content: @fa-var-shopping-cart; } -.@{fa-css-prefix}-folder:before { content: @fa-var-folder; } -.@{fa-css-prefix}-folder-open:before { content: @fa-var-folder-open; } -.@{fa-css-prefix}-arrows-v:before { content: @fa-var-arrows-v; } -.@{fa-css-prefix}-arrows-h:before { content: @fa-var-arrows-h; } -.@{fa-css-prefix}-bar-chart-o:before, -.@{fa-css-prefix}-bar-chart:before { content: @fa-var-bar-chart; } -.@{fa-css-prefix}-twitter-square:before { content: @fa-var-twitter-square; } -.@{fa-css-prefix}-facebook-square:before { content: @fa-var-facebook-square; } -.@{fa-css-prefix}-camera-retro:before { content: @fa-var-camera-retro; } -.@{fa-css-prefix}-key:before { content: @fa-var-key; } -.@{fa-css-prefix}-gears:before, -.@{fa-css-prefix}-cogs:before { content: @fa-var-cogs; } -.@{fa-css-prefix}-comments:before { content: @fa-var-comments; } -.@{fa-css-prefix}-thumbs-o-up:before { content: @fa-var-thumbs-o-up; } -.@{fa-css-prefix}-thumbs-o-down:before { content: @fa-var-thumbs-o-down; } -.@{fa-css-prefix}-star-half:before { content: @fa-var-star-half; } -.@{fa-css-prefix}-heart-o:before { content: @fa-var-heart-o; } -.@{fa-css-prefix}-sign-out:before { content: @fa-var-sign-out; } -.@{fa-css-prefix}-linkedin-square:before { content: @fa-var-linkedin-square; } -.@{fa-css-prefix}-thumb-tack:before { content: @fa-var-thumb-tack; } -.@{fa-css-prefix}-external-link:before { content: @fa-var-external-link; } -.@{fa-css-prefix}-sign-in:before { content: @fa-var-sign-in; } -.@{fa-css-prefix}-trophy:before { content: @fa-var-trophy; } -.@{fa-css-prefix}-github-square:before { content: @fa-var-github-square; } -.@{fa-css-prefix}-upload:before { content: @fa-var-upload; } -.@{fa-css-prefix}-lemon-o:before { content: @fa-var-lemon-o; } -.@{fa-css-prefix}-phone:before { content: @fa-var-phone; } -.@{fa-css-prefix}-square-o:before { content: @fa-var-square-o; } -.@{fa-css-prefix}-bookmark-o:before { content: @fa-var-bookmark-o; } -.@{fa-css-prefix}-phone-square:before { content: @fa-var-phone-square; } -.@{fa-css-prefix}-twitter:before { content: @fa-var-twitter; } -.@{fa-css-prefix}-facebook-f:before, -.@{fa-css-prefix}-facebook:before { content: @fa-var-facebook; } -.@{fa-css-prefix}-github:before { content: @fa-var-github; } -.@{fa-css-prefix}-unlock:before { content: @fa-var-unlock; } -.@{fa-css-prefix}-credit-card:before { content: @fa-var-credit-card; } -.@{fa-css-prefix}-feed:before, -.@{fa-css-prefix}-rss:before { content: @fa-var-rss; } -.@{fa-css-prefix}-hdd-o:before { content: @fa-var-hdd-o; } -.@{fa-css-prefix}-bullhorn:before { content: @fa-var-bullhorn; } -.@{fa-css-prefix}-bell:before { content: @fa-var-bell; } -.@{fa-css-prefix}-certificate:before { content: @fa-var-certificate; } -.@{fa-css-prefix}-hand-o-right:before { content: @fa-var-hand-o-right; } -.@{fa-css-prefix}-hand-o-left:before { content: @fa-var-hand-o-left; } -.@{fa-css-prefix}-hand-o-up:before { content: @fa-var-hand-o-up; } -.@{fa-css-prefix}-hand-o-down:before { content: @fa-var-hand-o-down; } -.@{fa-css-prefix}-arrow-circle-left:before { content: @fa-var-arrow-circle-left; } -.@{fa-css-prefix}-arrow-circle-right:before { content: @fa-var-arrow-circle-right; } -.@{fa-css-prefix}-arrow-circle-up:before { content: @fa-var-arrow-circle-up; } -.@{fa-css-prefix}-arrow-circle-down:before { content: @fa-var-arrow-circle-down; } -.@{fa-css-prefix}-globe:before { content: @fa-var-globe; } -.@{fa-css-prefix}-wrench:before { content: @fa-var-wrench; } -.@{fa-css-prefix}-tasks:before { content: @fa-var-tasks; } -.@{fa-css-prefix}-filter:before { content: @fa-var-filter; } -.@{fa-css-prefix}-briefcase:before { content: @fa-var-briefcase; } -.@{fa-css-prefix}-arrows-alt:before { content: @fa-var-arrows-alt; } -.@{fa-css-prefix}-group:before, -.@{fa-css-prefix}-users:before { content: @fa-var-users; } -.@{fa-css-prefix}-chain:before, -.@{fa-css-prefix}-link:before { content: @fa-var-link; } -.@{fa-css-prefix}-cloud:before { content: @fa-var-cloud; } -.@{fa-css-prefix}-flask:before { content: @fa-var-flask; } -.@{fa-css-prefix}-cut:before, -.@{fa-css-prefix}-scissors:before { content: @fa-var-scissors; } -.@{fa-css-prefix}-copy:before, -.@{fa-css-prefix}-files-o:before { content: @fa-var-files-o; } -.@{fa-css-prefix}-paperclip:before { content: @fa-var-paperclip; } -.@{fa-css-prefix}-save:before, -.@{fa-css-prefix}-floppy-o:before { content: @fa-var-floppy-o; } -.@{fa-css-prefix}-square:before { content: @fa-var-square; } -.@{fa-css-prefix}-navicon:before, -.@{fa-css-prefix}-reorder:before, -.@{fa-css-prefix}-bars:before { content: @fa-var-bars; } -.@{fa-css-prefix}-list-ul:before { content: @fa-var-list-ul; } -.@{fa-css-prefix}-list-ol:before { content: @fa-var-list-ol; } -.@{fa-css-prefix}-strikethrough:before { content: @fa-var-strikethrough; } -.@{fa-css-prefix}-underline:before { content: @fa-var-underline; } -.@{fa-css-prefix}-table:before { content: @fa-var-table; } -.@{fa-css-prefix}-magic:before { content: @fa-var-magic; } -.@{fa-css-prefix}-truck:before { content: @fa-var-truck; } -.@{fa-css-prefix}-pinterest:before { content: @fa-var-pinterest; } -.@{fa-css-prefix}-pinterest-square:before { content: @fa-var-pinterest-square; } -.@{fa-css-prefix}-google-plus-square:before { content: @fa-var-google-plus-square; } -.@{fa-css-prefix}-google-plus:before { content: @fa-var-google-plus; } -.@{fa-css-prefix}-money:before { content: @fa-var-money; } -.@{fa-css-prefix}-caret-down:before { content: @fa-var-caret-down; } -.@{fa-css-prefix}-caret-up:before { content: @fa-var-caret-up; } -.@{fa-css-prefix}-caret-left:before { content: @fa-var-caret-left; } -.@{fa-css-prefix}-caret-right:before { content: @fa-var-caret-right; } -.@{fa-css-prefix}-columns:before { content: @fa-var-columns; } -.@{fa-css-prefix}-unsorted:before, -.@{fa-css-prefix}-sort:before { content: @fa-var-sort; } -.@{fa-css-prefix}-sort-down:before, -.@{fa-css-prefix}-sort-desc:before { content: @fa-var-sort-desc; } -.@{fa-css-prefix}-sort-up:before, -.@{fa-css-prefix}-sort-asc:before { content: @fa-var-sort-asc; } -.@{fa-css-prefix}-envelope:before { content: @fa-var-envelope; } -.@{fa-css-prefix}-linkedin:before { content: @fa-var-linkedin; } -.@{fa-css-prefix}-rotate-left:before, -.@{fa-css-prefix}-undo:before { content: @fa-var-undo; } -.@{fa-css-prefix}-legal:before, -.@{fa-css-prefix}-gavel:before { content: @fa-var-gavel; } -.@{fa-css-prefix}-dashboard:before, -.@{fa-css-prefix}-tachometer:before { content: @fa-var-tachometer; } -.@{fa-css-prefix}-comment-o:before { content: @fa-var-comment-o; } -.@{fa-css-prefix}-comments-o:before { content: @fa-var-comments-o; } -.@{fa-css-prefix}-flash:before, -.@{fa-css-prefix}-bolt:before { content: @fa-var-bolt; } -.@{fa-css-prefix}-sitemap:before { content: @fa-var-sitemap; } -.@{fa-css-prefix}-umbrella:before { content: @fa-var-umbrella; } -.@{fa-css-prefix}-paste:before, -.@{fa-css-prefix}-clipboard:before { content: @fa-var-clipboard; } -.@{fa-css-prefix}-lightbulb-o:before { content: @fa-var-lightbulb-o; } -.@{fa-css-prefix}-exchange:before { content: @fa-var-exchange; } -.@{fa-css-prefix}-cloud-download:before { content: @fa-var-cloud-download; } -.@{fa-css-prefix}-cloud-upload:before { content: @fa-var-cloud-upload; } -.@{fa-css-prefix}-user-md:before { content: @fa-var-user-md; } -.@{fa-css-prefix}-stethoscope:before { content: @fa-var-stethoscope; } -.@{fa-css-prefix}-suitcase:before { content: @fa-var-suitcase; } -.@{fa-css-prefix}-bell-o:before { content: @fa-var-bell-o; } -.@{fa-css-prefix}-coffee:before { content: @fa-var-coffee; } -.@{fa-css-prefix}-cutlery:before { content: @fa-var-cutlery; } -.@{fa-css-prefix}-file-text-o:before { content: @fa-var-file-text-o; } -.@{fa-css-prefix}-building-o:before { content: @fa-var-building-o; } -.@{fa-css-prefix}-hospital-o:before { content: @fa-var-hospital-o; } -.@{fa-css-prefix}-ambulance:before { content: @fa-var-ambulance; } -.@{fa-css-prefix}-medkit:before { content: @fa-var-medkit; } -.@{fa-css-prefix}-fighter-jet:before { content: @fa-var-fighter-jet; } -.@{fa-css-prefix}-beer:before { content: @fa-var-beer; } -.@{fa-css-prefix}-h-square:before { content: @fa-var-h-square; } -.@{fa-css-prefix}-plus-square:before { content: @fa-var-plus-square; } -.@{fa-css-prefix}-angle-double-left:before { content: @fa-var-angle-double-left; } -.@{fa-css-prefix}-angle-double-right:before { content: @fa-var-angle-double-right; } -.@{fa-css-prefix}-angle-double-up:before { content: @fa-var-angle-double-up; } -.@{fa-css-prefix}-angle-double-down:before { content: @fa-var-angle-double-down; } -.@{fa-css-prefix}-angle-left:before { content: @fa-var-angle-left; } -.@{fa-css-prefix}-angle-right:before { content: @fa-var-angle-right; } -.@{fa-css-prefix}-angle-up:before { content: @fa-var-angle-up; } -.@{fa-css-prefix}-angle-down:before { content: @fa-var-angle-down; } -.@{fa-css-prefix}-desktop:before { content: @fa-var-desktop; } -.@{fa-css-prefix}-laptop:before { content: @fa-var-laptop; } -.@{fa-css-prefix}-tablet:before { content: @fa-var-tablet; } -.@{fa-css-prefix}-mobile-phone:before, -.@{fa-css-prefix}-mobile:before { content: @fa-var-mobile; } -.@{fa-css-prefix}-circle-o:before { content: @fa-var-circle-o; } -.@{fa-css-prefix}-quote-left:before { content: @fa-var-quote-left; } -.@{fa-css-prefix}-quote-right:before { content: @fa-var-quote-right; } -.@{fa-css-prefix}-spinner:before { content: @fa-var-spinner; } -.@{fa-css-prefix}-circle:before { content: @fa-var-circle; } -.@{fa-css-prefix}-mail-reply:before, -.@{fa-css-prefix}-reply:before { content: @fa-var-reply; } -.@{fa-css-prefix}-github-alt:before { content: @fa-var-github-alt; } -.@{fa-css-prefix}-folder-o:before { content: @fa-var-folder-o; } -.@{fa-css-prefix}-folder-open-o:before { content: @fa-var-folder-open-o; } -.@{fa-css-prefix}-smile-o:before { content: @fa-var-smile-o; } -.@{fa-css-prefix}-frown-o:before { content: @fa-var-frown-o; } -.@{fa-css-prefix}-meh-o:before { content: @fa-var-meh-o; } -.@{fa-css-prefix}-gamepad:before { content: @fa-var-gamepad; } -.@{fa-css-prefix}-keyboard-o:before { content: @fa-var-keyboard-o; } -.@{fa-css-prefix}-flag-o:before { content: @fa-var-flag-o; } -.@{fa-css-prefix}-flag-checkered:before { content: @fa-var-flag-checkered; } -.@{fa-css-prefix}-terminal:before { content: @fa-var-terminal; } -.@{fa-css-prefix}-code:before { content: @fa-var-code; } -.@{fa-css-prefix}-mail-reply-all:before, -.@{fa-css-prefix}-reply-all:before { content: @fa-var-reply-all; } -.@{fa-css-prefix}-star-half-empty:before, -.@{fa-css-prefix}-star-half-full:before, -.@{fa-css-prefix}-star-half-o:before { content: @fa-var-star-half-o; } -.@{fa-css-prefix}-location-arrow:before { content: @fa-var-location-arrow; } -.@{fa-css-prefix}-crop:before { content: @fa-var-crop; } -.@{fa-css-prefix}-code-fork:before { content: @fa-var-code-fork; } -.@{fa-css-prefix}-unlink:before, -.@{fa-css-prefix}-chain-broken:before { content: @fa-var-chain-broken; } -.@{fa-css-prefix}-question:before { content: @fa-var-question; } -.@{fa-css-prefix}-info:before { content: @fa-var-info; } -.@{fa-css-prefix}-exclamation:before { content: @fa-var-exclamation; } -.@{fa-css-prefix}-superscript:before { content: @fa-var-superscript; } -.@{fa-css-prefix}-subscript:before { content: @fa-var-subscript; } -.@{fa-css-prefix}-eraser:before { content: @fa-var-eraser; } -.@{fa-css-prefix}-puzzle-piece:before { content: @fa-var-puzzle-piece; } -.@{fa-css-prefix}-microphone:before { content: @fa-var-microphone; } -.@{fa-css-prefix}-microphone-slash:before { content: @fa-var-microphone-slash; } -.@{fa-css-prefix}-shield:before { content: @fa-var-shield; } -.@{fa-css-prefix}-calendar-o:before { content: @fa-var-calendar-o; } -.@{fa-css-prefix}-fire-extinguisher:before { content: @fa-var-fire-extinguisher; } -.@{fa-css-prefix}-rocket:before { content: @fa-var-rocket; } -.@{fa-css-prefix}-maxcdn:before { content: @fa-var-maxcdn; } -.@{fa-css-prefix}-chevron-circle-left:before { content: @fa-var-chevron-circle-left; } -.@{fa-css-prefix}-chevron-circle-right:before { content: @fa-var-chevron-circle-right; } -.@{fa-css-prefix}-chevron-circle-up:before { content: @fa-var-chevron-circle-up; } -.@{fa-css-prefix}-chevron-circle-down:before { content: @fa-var-chevron-circle-down; } -.@{fa-css-prefix}-html5:before { content: @fa-var-html5; } -.@{fa-css-prefix}-css3:before { content: @fa-var-css3; } -.@{fa-css-prefix}-anchor:before { content: @fa-var-anchor; } -.@{fa-css-prefix}-unlock-alt:before { content: @fa-var-unlock-alt; } -.@{fa-css-prefix}-bullseye:before { content: @fa-var-bullseye; } -.@{fa-css-prefix}-ellipsis-h:before { content: @fa-var-ellipsis-h; } -.@{fa-css-prefix}-ellipsis-v:before { content: @fa-var-ellipsis-v; } -.@{fa-css-prefix}-rss-square:before { content: @fa-var-rss-square; } -.@{fa-css-prefix}-play-circle:before { content: @fa-var-play-circle; } -.@{fa-css-prefix}-ticket:before { content: @fa-var-ticket; } -.@{fa-css-prefix}-minus-square:before { content: @fa-var-minus-square; } -.@{fa-css-prefix}-minus-square-o:before { content: @fa-var-minus-square-o; } -.@{fa-css-prefix}-level-up:before { content: @fa-var-level-up; } -.@{fa-css-prefix}-level-down:before { content: @fa-var-level-down; } -.@{fa-css-prefix}-check-square:before { content: @fa-var-check-square; } -.@{fa-css-prefix}-pencil-square:before { content: @fa-var-pencil-square; } -.@{fa-css-prefix}-external-link-square:before { content: @fa-var-external-link-square; } -.@{fa-css-prefix}-share-square:before { content: @fa-var-share-square; } -.@{fa-css-prefix}-compass:before { content: @fa-var-compass; } -.@{fa-css-prefix}-toggle-down:before, -.@{fa-css-prefix}-caret-square-o-down:before { content: @fa-var-caret-square-o-down; } -.@{fa-css-prefix}-toggle-up:before, -.@{fa-css-prefix}-caret-square-o-up:before { content: @fa-var-caret-square-o-up; } -.@{fa-css-prefix}-toggle-right:before, -.@{fa-css-prefix}-caret-square-o-right:before { content: @fa-var-caret-square-o-right; } -.@{fa-css-prefix}-euro:before, -.@{fa-css-prefix}-eur:before { content: @fa-var-eur; } -.@{fa-css-prefix}-gbp:before { content: @fa-var-gbp; } -.@{fa-css-prefix}-dollar:before, -.@{fa-css-prefix}-usd:before { content: @fa-var-usd; } -.@{fa-css-prefix}-rupee:before, -.@{fa-css-prefix}-inr:before { content: @fa-var-inr; } -.@{fa-css-prefix}-cny:before, -.@{fa-css-prefix}-rmb:before, -.@{fa-css-prefix}-yen:before, -.@{fa-css-prefix}-jpy:before { content: @fa-var-jpy; } -.@{fa-css-prefix}-ruble:before, -.@{fa-css-prefix}-rouble:before, -.@{fa-css-prefix}-rub:before { content: @fa-var-rub; } -.@{fa-css-prefix}-won:before, -.@{fa-css-prefix}-krw:before { content: @fa-var-krw; } -.@{fa-css-prefix}-bitcoin:before, -.@{fa-css-prefix}-btc:before { content: @fa-var-btc; } -.@{fa-css-prefix}-file:before { content: @fa-var-file; } -.@{fa-css-prefix}-file-text:before { content: @fa-var-file-text; } -.@{fa-css-prefix}-sort-alpha-asc:before { content: @fa-var-sort-alpha-asc; } -.@{fa-css-prefix}-sort-alpha-desc:before { content: @fa-var-sort-alpha-desc; } -.@{fa-css-prefix}-sort-amount-asc:before { content: @fa-var-sort-amount-asc; } -.@{fa-css-prefix}-sort-amount-desc:before { content: @fa-var-sort-amount-desc; } -.@{fa-css-prefix}-sort-numeric-asc:before { content: @fa-var-sort-numeric-asc; } -.@{fa-css-prefix}-sort-numeric-desc:before { content: @fa-var-sort-numeric-desc; } -.@{fa-css-prefix}-thumbs-up:before { content: @fa-var-thumbs-up; } -.@{fa-css-prefix}-thumbs-down:before { content: @fa-var-thumbs-down; } -.@{fa-css-prefix}-youtube-square:before { content: @fa-var-youtube-square; } -.@{fa-css-prefix}-youtube:before { content: @fa-var-youtube; } -.@{fa-css-prefix}-xing:before { content: @fa-var-xing; } -.@{fa-css-prefix}-xing-square:before { content: @fa-var-xing-square; } -.@{fa-css-prefix}-youtube-play:before { content: @fa-var-youtube-play; } -.@{fa-css-prefix}-dropbox:before { content: @fa-var-dropbox; } -.@{fa-css-prefix}-stack-overflow:before { content: @fa-var-stack-overflow; } -.@{fa-css-prefix}-instagram:before { content: @fa-var-instagram; } -.@{fa-css-prefix}-flickr:before { content: @fa-var-flickr; } -.@{fa-css-prefix}-adn:before { content: @fa-var-adn; } -.@{fa-css-prefix}-bitbucket:before { content: @fa-var-bitbucket; } -.@{fa-css-prefix}-bitbucket-square:before { content: @fa-var-bitbucket-square; } -.@{fa-css-prefix}-tumblr:before { content: @fa-var-tumblr; } -.@{fa-css-prefix}-tumblr-square:before { content: @fa-var-tumblr-square; } -.@{fa-css-prefix}-long-arrow-down:before { content: @fa-var-long-arrow-down; } -.@{fa-css-prefix}-long-arrow-up:before { content: @fa-var-long-arrow-up; } -.@{fa-css-prefix}-long-arrow-left:before { content: @fa-var-long-arrow-left; } -.@{fa-css-prefix}-long-arrow-right:before { content: @fa-var-long-arrow-right; } -.@{fa-css-prefix}-apple:before { content: @fa-var-apple; } -.@{fa-css-prefix}-windows:before { content: @fa-var-windows; } -.@{fa-css-prefix}-android:before { content: @fa-var-android; } -.@{fa-css-prefix}-linux:before { content: @fa-var-linux; } -.@{fa-css-prefix}-dribbble:before { content: @fa-var-dribbble; } -.@{fa-css-prefix}-skype:before { content: @fa-var-skype; } -.@{fa-css-prefix}-foursquare:before { content: @fa-var-foursquare; } -.@{fa-css-prefix}-trello:before { content: @fa-var-trello; } -.@{fa-css-prefix}-female:before { content: @fa-var-female; } -.@{fa-css-prefix}-male:before { content: @fa-var-male; } -.@{fa-css-prefix}-gittip:before, -.@{fa-css-prefix}-gratipay:before { content: @fa-var-gratipay; } -.@{fa-css-prefix}-sun-o:before { content: @fa-var-sun-o; } -.@{fa-css-prefix}-moon-o:before { content: @fa-var-moon-o; } -.@{fa-css-prefix}-archive:before { content: @fa-var-archive; } -.@{fa-css-prefix}-bug:before { content: @fa-var-bug; } -.@{fa-css-prefix}-vk:before { content: @fa-var-vk; } -.@{fa-css-prefix}-weibo:before { content: @fa-var-weibo; } -.@{fa-css-prefix}-renren:before { content: @fa-var-renren; } -.@{fa-css-prefix}-pagelines:before { content: @fa-var-pagelines; } -.@{fa-css-prefix}-stack-exchange:before { content: @fa-var-stack-exchange; } -.@{fa-css-prefix}-arrow-circle-o-right:before { content: @fa-var-arrow-circle-o-right; } -.@{fa-css-prefix}-arrow-circle-o-left:before { content: @fa-var-arrow-circle-o-left; } -.@{fa-css-prefix}-toggle-left:before, -.@{fa-css-prefix}-caret-square-o-left:before { content: @fa-var-caret-square-o-left; } -.@{fa-css-prefix}-dot-circle-o:before { content: @fa-var-dot-circle-o; } -.@{fa-css-prefix}-wheelchair:before { content: @fa-var-wheelchair; } -.@{fa-css-prefix}-vimeo-square:before { content: @fa-var-vimeo-square; } -.@{fa-css-prefix}-turkish-lira:before, -.@{fa-css-prefix}-try:before { content: @fa-var-try; } -.@{fa-css-prefix}-plus-square-o:before { content: @fa-var-plus-square-o; } -.@{fa-css-prefix}-space-shuttle:before { content: @fa-var-space-shuttle; } -.@{fa-css-prefix}-slack:before { content: @fa-var-slack; } -.@{fa-css-prefix}-envelope-square:before { content: @fa-var-envelope-square; } -.@{fa-css-prefix}-wordpress:before { content: @fa-var-wordpress; } -.@{fa-css-prefix}-openid:before { content: @fa-var-openid; } -.@{fa-css-prefix}-institution:before, -.@{fa-css-prefix}-bank:before, -.@{fa-css-prefix}-university:before { content: @fa-var-university; } -.@{fa-css-prefix}-mortar-board:before, -.@{fa-css-prefix}-graduation-cap:before { content: @fa-var-graduation-cap; } -.@{fa-css-prefix}-yahoo:before { content: @fa-var-yahoo; } -.@{fa-css-prefix}-google:before { content: @fa-var-google; } -.@{fa-css-prefix}-reddit:before { content: @fa-var-reddit; } -.@{fa-css-prefix}-reddit-square:before { content: @fa-var-reddit-square; } -.@{fa-css-prefix}-stumbleupon-circle:before { content: @fa-var-stumbleupon-circle; } -.@{fa-css-prefix}-stumbleupon:before { content: @fa-var-stumbleupon; } -.@{fa-css-prefix}-delicious:before { content: @fa-var-delicious; } -.@{fa-css-prefix}-digg:before { content: @fa-var-digg; } -.@{fa-css-prefix}-pied-piper-pp:before { content: @fa-var-pied-piper-pp; } -.@{fa-css-prefix}-pied-piper-alt:before { content: @fa-var-pied-piper-alt; } -.@{fa-css-prefix}-drupal:before { content: @fa-var-drupal; } -.@{fa-css-prefix}-joomla:before { content: @fa-var-joomla; } -.@{fa-css-prefix}-language:before { content: @fa-var-language; } -.@{fa-css-prefix}-fax:before { content: @fa-var-fax; } -.@{fa-css-prefix}-building:before { content: @fa-var-building; } -.@{fa-css-prefix}-child:before { content: @fa-var-child; } -.@{fa-css-prefix}-paw:before { content: @fa-var-paw; } -.@{fa-css-prefix}-spoon:before { content: @fa-var-spoon; } -.@{fa-css-prefix}-cube:before { content: @fa-var-cube; } -.@{fa-css-prefix}-cubes:before { content: @fa-var-cubes; } -.@{fa-css-prefix}-behance:before { content: @fa-var-behance; } -.@{fa-css-prefix}-behance-square:before { content: @fa-var-behance-square; } -.@{fa-css-prefix}-steam:before { content: @fa-var-steam; } -.@{fa-css-prefix}-steam-square:before { content: @fa-var-steam-square; } -.@{fa-css-prefix}-recycle:before { content: @fa-var-recycle; } -.@{fa-css-prefix}-automobile:before, -.@{fa-css-prefix}-car:before { content: @fa-var-car; } -.@{fa-css-prefix}-cab:before, -.@{fa-css-prefix}-taxi:before { content: @fa-var-taxi; } -.@{fa-css-prefix}-tree:before { content: @fa-var-tree; } -.@{fa-css-prefix}-spotify:before { content: @fa-var-spotify; } -.@{fa-css-prefix}-deviantart:before { content: @fa-var-deviantart; } -.@{fa-css-prefix}-soundcloud:before { content: @fa-var-soundcloud; } -.@{fa-css-prefix}-database:before { content: @fa-var-database; } -.@{fa-css-prefix}-file-pdf-o:before { content: @fa-var-file-pdf-o; } -.@{fa-css-prefix}-file-word-o:before { content: @fa-var-file-word-o; } -.@{fa-css-prefix}-file-excel-o:before { content: @fa-var-file-excel-o; } -.@{fa-css-prefix}-file-powerpoint-o:before { content: @fa-var-file-powerpoint-o; } -.@{fa-css-prefix}-file-photo-o:before, -.@{fa-css-prefix}-file-picture-o:before, -.@{fa-css-prefix}-file-image-o:before { content: @fa-var-file-image-o; } -.@{fa-css-prefix}-file-zip-o:before, -.@{fa-css-prefix}-file-archive-o:before { content: @fa-var-file-archive-o; } -.@{fa-css-prefix}-file-sound-o:before, -.@{fa-css-prefix}-file-audio-o:before { content: @fa-var-file-audio-o; } -.@{fa-css-prefix}-file-movie-o:before, -.@{fa-css-prefix}-file-video-o:before { content: @fa-var-file-video-o; } -.@{fa-css-prefix}-file-code-o:before { content: @fa-var-file-code-o; } -.@{fa-css-prefix}-vine:before { content: @fa-var-vine; } -.@{fa-css-prefix}-codepen:before { content: @fa-var-codepen; } -.@{fa-css-prefix}-jsfiddle:before { content: @fa-var-jsfiddle; } -.@{fa-css-prefix}-life-bouy:before, -.@{fa-css-prefix}-life-buoy:before, -.@{fa-css-prefix}-life-saver:before, -.@{fa-css-prefix}-support:before, -.@{fa-css-prefix}-life-ring:before { content: @fa-var-life-ring; } -.@{fa-css-prefix}-circle-o-notch:before { content: @fa-var-circle-o-notch; } -.@{fa-css-prefix}-ra:before, -.@{fa-css-prefix}-resistance:before, -.@{fa-css-prefix}-rebel:before { content: @fa-var-rebel; } -.@{fa-css-prefix}-ge:before, -.@{fa-css-prefix}-empire:before { content: @fa-var-empire; } -.@{fa-css-prefix}-git-square:before { content: @fa-var-git-square; } -.@{fa-css-prefix}-git:before { content: @fa-var-git; } -.@{fa-css-prefix}-y-combinator-square:before, -.@{fa-css-prefix}-yc-square:before, -.@{fa-css-prefix}-hacker-news:before { content: @fa-var-hacker-news; } -.@{fa-css-prefix}-tencent-weibo:before { content: @fa-var-tencent-weibo; } -.@{fa-css-prefix}-qq:before { content: @fa-var-qq; } -.@{fa-css-prefix}-wechat:before, -.@{fa-css-prefix}-weixin:before { content: @fa-var-weixin; } -.@{fa-css-prefix}-send:before, -.@{fa-css-prefix}-paper-plane:before { content: @fa-var-paper-plane; } -.@{fa-css-prefix}-send-o:before, -.@{fa-css-prefix}-paper-plane-o:before { content: @fa-var-paper-plane-o; } -.@{fa-css-prefix}-history:before { content: @fa-var-history; } -.@{fa-css-prefix}-circle-thin:before { content: @fa-var-circle-thin; } -.@{fa-css-prefix}-header:before { content: @fa-var-header; } -.@{fa-css-prefix}-paragraph:before { content: @fa-var-paragraph; } -.@{fa-css-prefix}-sliders:before { content: @fa-var-sliders; } -.@{fa-css-prefix}-share-alt:before { content: @fa-var-share-alt; } -.@{fa-css-prefix}-share-alt-square:before { content: @fa-var-share-alt-square; } -.@{fa-css-prefix}-bomb:before { content: @fa-var-bomb; } -.@{fa-css-prefix}-soccer-ball-o:before, -.@{fa-css-prefix}-futbol-o:before { content: @fa-var-futbol-o; } -.@{fa-css-prefix}-tty:before { content: @fa-var-tty; } -.@{fa-css-prefix}-binoculars:before { content: @fa-var-binoculars; } -.@{fa-css-prefix}-plug:before { content: @fa-var-plug; } -.@{fa-css-prefix}-slideshare:before { content: @fa-var-slideshare; } -.@{fa-css-prefix}-twitch:before { content: @fa-var-twitch; } -.@{fa-css-prefix}-yelp:before { content: @fa-var-yelp; } -.@{fa-css-prefix}-newspaper-o:before { content: @fa-var-newspaper-o; } -.@{fa-css-prefix}-wifi:before { content: @fa-var-wifi; } -.@{fa-css-prefix}-calculator:before { content: @fa-var-calculator; } -.@{fa-css-prefix}-paypal:before { content: @fa-var-paypal; } -.@{fa-css-prefix}-google-wallet:before { content: @fa-var-google-wallet; } -.@{fa-css-prefix}-cc-visa:before { content: @fa-var-cc-visa; } -.@{fa-css-prefix}-cc-mastercard:before { content: @fa-var-cc-mastercard; } -.@{fa-css-prefix}-cc-discover:before { content: @fa-var-cc-discover; } -.@{fa-css-prefix}-cc-amex:before { content: @fa-var-cc-amex; } -.@{fa-css-prefix}-cc-paypal:before { content: @fa-var-cc-paypal; } -.@{fa-css-prefix}-cc-stripe:before { content: @fa-var-cc-stripe; } -.@{fa-css-prefix}-bell-slash:before { content: @fa-var-bell-slash; } -.@{fa-css-prefix}-bell-slash-o:before { content: @fa-var-bell-slash-o; } -.@{fa-css-prefix}-trash:before { content: @fa-var-trash; } -.@{fa-css-prefix}-copyright:before { content: @fa-var-copyright; } -.@{fa-css-prefix}-at:before { content: @fa-var-at; } -.@{fa-css-prefix}-eyedropper:before { content: @fa-var-eyedropper; } -.@{fa-css-prefix}-paint-brush:before { content: @fa-var-paint-brush; } -.@{fa-css-prefix}-birthday-cake:before { content: @fa-var-birthday-cake; } -.@{fa-css-prefix}-area-chart:before { content: @fa-var-area-chart; } -.@{fa-css-prefix}-pie-chart:before { content: @fa-var-pie-chart; } -.@{fa-css-prefix}-line-chart:before { content: @fa-var-line-chart; } -.@{fa-css-prefix}-lastfm:before { content: @fa-var-lastfm; } -.@{fa-css-prefix}-lastfm-square:before { content: @fa-var-lastfm-square; } -.@{fa-css-prefix}-toggle-off:before { content: @fa-var-toggle-off; } -.@{fa-css-prefix}-toggle-on:before { content: @fa-var-toggle-on; } -.@{fa-css-prefix}-bicycle:before { content: @fa-var-bicycle; } -.@{fa-css-prefix}-bus:before { content: @fa-var-bus; } -.@{fa-css-prefix}-ioxhost:before { content: @fa-var-ioxhost; } -.@{fa-css-prefix}-angellist:before { content: @fa-var-angellist; } -.@{fa-css-prefix}-cc:before { content: @fa-var-cc; } -.@{fa-css-prefix}-shekel:before, -.@{fa-css-prefix}-sheqel:before, -.@{fa-css-prefix}-ils:before { content: @fa-var-ils; } -.@{fa-css-prefix}-meanpath:before { content: @fa-var-meanpath; } -.@{fa-css-prefix}-buysellads:before { content: @fa-var-buysellads; } -.@{fa-css-prefix}-connectdevelop:before { content: @fa-var-connectdevelop; } -.@{fa-css-prefix}-dashcube:before { content: @fa-var-dashcube; } -.@{fa-css-prefix}-forumbee:before { content: @fa-var-forumbee; } -.@{fa-css-prefix}-leanpub:before { content: @fa-var-leanpub; } -.@{fa-css-prefix}-sellsy:before { content: @fa-var-sellsy; } -.@{fa-css-prefix}-shirtsinbulk:before { content: @fa-var-shirtsinbulk; } -.@{fa-css-prefix}-simplybuilt:before { content: @fa-var-simplybuilt; } -.@{fa-css-prefix}-skyatlas:before { content: @fa-var-skyatlas; } -.@{fa-css-prefix}-cart-plus:before { content: @fa-var-cart-plus; } -.@{fa-css-prefix}-cart-arrow-down:before { content: @fa-var-cart-arrow-down; } -.@{fa-css-prefix}-diamond:before { content: @fa-var-diamond; } -.@{fa-css-prefix}-ship:before { content: @fa-var-ship; } -.@{fa-css-prefix}-user-secret:before { content: @fa-var-user-secret; } -.@{fa-css-prefix}-motorcycle:before { content: @fa-var-motorcycle; } -.@{fa-css-prefix}-street-view:before { content: @fa-var-street-view; } -.@{fa-css-prefix}-heartbeat:before { content: @fa-var-heartbeat; } -.@{fa-css-prefix}-venus:before { content: @fa-var-venus; } -.@{fa-css-prefix}-mars:before { content: @fa-var-mars; } -.@{fa-css-prefix}-mercury:before { content: @fa-var-mercury; } -.@{fa-css-prefix}-intersex:before, -.@{fa-css-prefix}-transgender:before { content: @fa-var-transgender; } -.@{fa-css-prefix}-transgender-alt:before { content: @fa-var-transgender-alt; } -.@{fa-css-prefix}-venus-double:before { content: @fa-var-venus-double; } -.@{fa-css-prefix}-mars-double:before { content: @fa-var-mars-double; } -.@{fa-css-prefix}-venus-mars:before { content: @fa-var-venus-mars; } -.@{fa-css-prefix}-mars-stroke:before { content: @fa-var-mars-stroke; } -.@{fa-css-prefix}-mars-stroke-v:before { content: @fa-var-mars-stroke-v; } -.@{fa-css-prefix}-mars-stroke-h:before { content: @fa-var-mars-stroke-h; } -.@{fa-css-prefix}-neuter:before { content: @fa-var-neuter; } -.@{fa-css-prefix}-genderless:before { content: @fa-var-genderless; } -.@{fa-css-prefix}-facebook-official:before { content: @fa-var-facebook-official; } -.@{fa-css-prefix}-pinterest-p:before { content: @fa-var-pinterest-p; } -.@{fa-css-prefix}-whatsapp:before { content: @fa-var-whatsapp; } -.@{fa-css-prefix}-server:before { content: @fa-var-server; } -.@{fa-css-prefix}-user-plus:before { content: @fa-var-user-plus; } -.@{fa-css-prefix}-user-times:before { content: @fa-var-user-times; } -.@{fa-css-prefix}-hotel:before, -.@{fa-css-prefix}-bed:before { content: @fa-var-bed; } -.@{fa-css-prefix}-viacoin:before { content: @fa-var-viacoin; } -.@{fa-css-prefix}-train:before { content: @fa-var-train; } -.@{fa-css-prefix}-subway:before { content: @fa-var-subway; } -.@{fa-css-prefix}-medium:before { content: @fa-var-medium; } -.@{fa-css-prefix}-yc:before, -.@{fa-css-prefix}-y-combinator:before { content: @fa-var-y-combinator; } -.@{fa-css-prefix}-optin-monster:before { content: @fa-var-optin-monster; } -.@{fa-css-prefix}-opencart:before { content: @fa-var-opencart; } -.@{fa-css-prefix}-expeditedssl:before { content: @fa-var-expeditedssl; } -.@{fa-css-prefix}-battery-4:before, -.@{fa-css-prefix}-battery:before, -.@{fa-css-prefix}-battery-full:before { content: @fa-var-battery-full; } -.@{fa-css-prefix}-battery-3:before, -.@{fa-css-prefix}-battery-three-quarters:before { content: @fa-var-battery-three-quarters; } -.@{fa-css-prefix}-battery-2:before, -.@{fa-css-prefix}-battery-half:before { content: @fa-var-battery-half; } -.@{fa-css-prefix}-battery-1:before, -.@{fa-css-prefix}-battery-quarter:before { content: @fa-var-battery-quarter; } -.@{fa-css-prefix}-battery-0:before, -.@{fa-css-prefix}-battery-empty:before { content: @fa-var-battery-empty; } -.@{fa-css-prefix}-mouse-pointer:before { content: @fa-var-mouse-pointer; } -.@{fa-css-prefix}-i-cursor:before { content: @fa-var-i-cursor; } -.@{fa-css-prefix}-object-group:before { content: @fa-var-object-group; } -.@{fa-css-prefix}-object-ungroup:before { content: @fa-var-object-ungroup; } -.@{fa-css-prefix}-sticky-note:before { content: @fa-var-sticky-note; } -.@{fa-css-prefix}-sticky-note-o:before { content: @fa-var-sticky-note-o; } -.@{fa-css-prefix}-cc-jcb:before { content: @fa-var-cc-jcb; } -.@{fa-css-prefix}-cc-diners-club:before { content: @fa-var-cc-diners-club; } -.@{fa-css-prefix}-clone:before { content: @fa-var-clone; } -.@{fa-css-prefix}-balance-scale:before { content: @fa-var-balance-scale; } -.@{fa-css-prefix}-hourglass-o:before { content: @fa-var-hourglass-o; } -.@{fa-css-prefix}-hourglass-1:before, -.@{fa-css-prefix}-hourglass-start:before { content: @fa-var-hourglass-start; } -.@{fa-css-prefix}-hourglass-2:before, -.@{fa-css-prefix}-hourglass-half:before { content: @fa-var-hourglass-half; } -.@{fa-css-prefix}-hourglass-3:before, -.@{fa-css-prefix}-hourglass-end:before { content: @fa-var-hourglass-end; } -.@{fa-css-prefix}-hourglass:before { content: @fa-var-hourglass; } -.@{fa-css-prefix}-hand-grab-o:before, -.@{fa-css-prefix}-hand-rock-o:before { content: @fa-var-hand-rock-o; } -.@{fa-css-prefix}-hand-stop-o:before, -.@{fa-css-prefix}-hand-paper-o:before { content: @fa-var-hand-paper-o; } -.@{fa-css-prefix}-hand-scissors-o:before { content: @fa-var-hand-scissors-o; } -.@{fa-css-prefix}-hand-lizard-o:before { content: @fa-var-hand-lizard-o; } -.@{fa-css-prefix}-hand-spock-o:before { content: @fa-var-hand-spock-o; } -.@{fa-css-prefix}-hand-pointer-o:before { content: @fa-var-hand-pointer-o; } -.@{fa-css-prefix}-hand-peace-o:before { content: @fa-var-hand-peace-o; } -.@{fa-css-prefix}-trademark:before { content: @fa-var-trademark; } -.@{fa-css-prefix}-registered:before { content: @fa-var-registered; } -.@{fa-css-prefix}-creative-commons:before { content: @fa-var-creative-commons; } -.@{fa-css-prefix}-gg:before { content: @fa-var-gg; } -.@{fa-css-prefix}-gg-circle:before { content: @fa-var-gg-circle; } -.@{fa-css-prefix}-tripadvisor:before { content: @fa-var-tripadvisor; } -.@{fa-css-prefix}-odnoklassniki:before { content: @fa-var-odnoklassniki; } -.@{fa-css-prefix}-odnoklassniki-square:before { content: @fa-var-odnoklassniki-square; } -.@{fa-css-prefix}-get-pocket:before { content: @fa-var-get-pocket; } -.@{fa-css-prefix}-wikipedia-w:before { content: @fa-var-wikipedia-w; } -.@{fa-css-prefix}-safari:before { content: @fa-var-safari; } -.@{fa-css-prefix}-chrome:before { content: @fa-var-chrome; } -.@{fa-css-prefix}-firefox:before { content: @fa-var-firefox; } -.@{fa-css-prefix}-opera:before { content: @fa-var-opera; } -.@{fa-css-prefix}-internet-explorer:before { content: @fa-var-internet-explorer; } -.@{fa-css-prefix}-tv:before, -.@{fa-css-prefix}-television:before { content: @fa-var-television; } -.@{fa-css-prefix}-contao:before { content: @fa-var-contao; } -.@{fa-css-prefix}-500px:before { content: @fa-var-500px; } -.@{fa-css-prefix}-amazon:before { content: @fa-var-amazon; } -.@{fa-css-prefix}-calendar-plus-o:before { content: @fa-var-calendar-plus-o; } -.@{fa-css-prefix}-calendar-minus-o:before { content: @fa-var-calendar-minus-o; } -.@{fa-css-prefix}-calendar-times-o:before { content: @fa-var-calendar-times-o; } -.@{fa-css-prefix}-calendar-check-o:before { content: @fa-var-calendar-check-o; } -.@{fa-css-prefix}-industry:before { content: @fa-var-industry; } -.@{fa-css-prefix}-map-pin:before { content: @fa-var-map-pin; } -.@{fa-css-prefix}-map-signs:before { content: @fa-var-map-signs; } -.@{fa-css-prefix}-map-o:before { content: @fa-var-map-o; } -.@{fa-css-prefix}-map:before { content: @fa-var-map; } -.@{fa-css-prefix}-commenting:before { content: @fa-var-commenting; } -.@{fa-css-prefix}-commenting-o:before { content: @fa-var-commenting-o; } -.@{fa-css-prefix}-houzz:before { content: @fa-var-houzz; } -.@{fa-css-prefix}-vimeo:before { content: @fa-var-vimeo; } -.@{fa-css-prefix}-black-tie:before { content: @fa-var-black-tie; } -.@{fa-css-prefix}-fonticons:before { content: @fa-var-fonticons; } -.@{fa-css-prefix}-reddit-alien:before { content: @fa-var-reddit-alien; } -.@{fa-css-prefix}-edge:before { content: @fa-var-edge; } -.@{fa-css-prefix}-credit-card-alt:before { content: @fa-var-credit-card-alt; } -.@{fa-css-prefix}-codiepie:before { content: @fa-var-codiepie; } -.@{fa-css-prefix}-modx:before { content: @fa-var-modx; } -.@{fa-css-prefix}-fort-awesome:before { content: @fa-var-fort-awesome; } -.@{fa-css-prefix}-usb:before { content: @fa-var-usb; } -.@{fa-css-prefix}-product-hunt:before { content: @fa-var-product-hunt; } -.@{fa-css-prefix}-mixcloud:before { content: @fa-var-mixcloud; } -.@{fa-css-prefix}-scribd:before { content: @fa-var-scribd; } -.@{fa-css-prefix}-pause-circle:before { content: @fa-var-pause-circle; } -.@{fa-css-prefix}-pause-circle-o:before { content: @fa-var-pause-circle-o; } -.@{fa-css-prefix}-stop-circle:before { content: @fa-var-stop-circle; } -.@{fa-css-prefix}-stop-circle-o:before { content: @fa-var-stop-circle-o; } -.@{fa-css-prefix}-shopping-bag:before { content: @fa-var-shopping-bag; } -.@{fa-css-prefix}-shopping-basket:before { content: @fa-var-shopping-basket; } -.@{fa-css-prefix}-hashtag:before { content: @fa-var-hashtag; } -.@{fa-css-prefix}-bluetooth:before { content: @fa-var-bluetooth; } -.@{fa-css-prefix}-bluetooth-b:before { content: @fa-var-bluetooth-b; } -.@{fa-css-prefix}-percent:before { content: @fa-var-percent; } -.@{fa-css-prefix}-gitlab:before { content: @fa-var-gitlab; } -.@{fa-css-prefix}-wpbeginner:before { content: @fa-var-wpbeginner; } -.@{fa-css-prefix}-wpforms:before { content: @fa-var-wpforms; } -.@{fa-css-prefix}-envira:before { content: @fa-var-envira; } -.@{fa-css-prefix}-universal-access:before { content: @fa-var-universal-access; } -.@{fa-css-prefix}-wheelchair-alt:before { content: @fa-var-wheelchair-alt; } -.@{fa-css-prefix}-question-circle-o:before { content: @fa-var-question-circle-o; } -.@{fa-css-prefix}-blind:before { content: @fa-var-blind; } -.@{fa-css-prefix}-audio-description:before { content: @fa-var-audio-description; } -.@{fa-css-prefix}-volume-control-phone:before { content: @fa-var-volume-control-phone; } -.@{fa-css-prefix}-braille:before { content: @fa-var-braille; } -.@{fa-css-prefix}-assistive-listening-systems:before { content: @fa-var-assistive-listening-systems; } -.@{fa-css-prefix}-asl-interpreting:before, -.@{fa-css-prefix}-american-sign-language-interpreting:before { content: @fa-var-american-sign-language-interpreting; } -.@{fa-css-prefix}-deafness:before, -.@{fa-css-prefix}-hard-of-hearing:before, -.@{fa-css-prefix}-deaf:before { content: @fa-var-deaf; } -.@{fa-css-prefix}-glide:before { content: @fa-var-glide; } -.@{fa-css-prefix}-glide-g:before { content: @fa-var-glide-g; } -.@{fa-css-prefix}-signing:before, -.@{fa-css-prefix}-sign-language:before { content: @fa-var-sign-language; } -.@{fa-css-prefix}-low-vision:before { content: @fa-var-low-vision; } -.@{fa-css-prefix}-viadeo:before { content: @fa-var-viadeo; } -.@{fa-css-prefix}-viadeo-square:before { content: @fa-var-viadeo-square; } -.@{fa-css-prefix}-snapchat:before { content: @fa-var-snapchat; } -.@{fa-css-prefix}-snapchat-ghost:before { content: @fa-var-snapchat-ghost; } -.@{fa-css-prefix}-snapchat-square:before { content: @fa-var-snapchat-square; } -.@{fa-css-prefix}-pied-piper:before { content: @fa-var-pied-piper; } -.@{fa-css-prefix}-first-order:before { content: @fa-var-first-order; } -.@{fa-css-prefix}-yoast:before { content: @fa-var-yoast; } -.@{fa-css-prefix}-themeisle:before { content: @fa-var-themeisle; } -.@{fa-css-prefix}-google-plus-circle:before, -.@{fa-css-prefix}-google-plus-official:before { content: @fa-var-google-plus-official; } -.@{fa-css-prefix}-fa:before, -.@{fa-css-prefix}-font-awesome:before { content: @fa-var-font-awesome; } -.@{fa-css-prefix}-handshake-o:before { content: @fa-var-handshake-o; } -.@{fa-css-prefix}-envelope-open:before { content: @fa-var-envelope-open; } -.@{fa-css-prefix}-envelope-open-o:before { content: @fa-var-envelope-open-o; } -.@{fa-css-prefix}-linode:before { content: @fa-var-linode; } -.@{fa-css-prefix}-address-book:before { content: @fa-var-address-book; } -.@{fa-css-prefix}-address-book-o:before { content: @fa-var-address-book-o; } -.@{fa-css-prefix}-vcard:before, -.@{fa-css-prefix}-address-card:before { content: @fa-var-address-card; } -.@{fa-css-prefix}-vcard-o:before, -.@{fa-css-prefix}-address-card-o:before { content: @fa-var-address-card-o; } -.@{fa-css-prefix}-user-circle:before { content: @fa-var-user-circle; } -.@{fa-css-prefix}-user-circle-o:before { content: @fa-var-user-circle-o; } -.@{fa-css-prefix}-user-o:before { content: @fa-var-user-o; } -.@{fa-css-prefix}-id-badge:before { content: @fa-var-id-badge; } -.@{fa-css-prefix}-drivers-license:before, -.@{fa-css-prefix}-id-card:before { content: @fa-var-id-card; } -.@{fa-css-prefix}-drivers-license-o:before, -.@{fa-css-prefix}-id-card-o:before { content: @fa-var-id-card-o; } -.@{fa-css-prefix}-quora:before { content: @fa-var-quora; } -.@{fa-css-prefix}-free-code-camp:before { content: @fa-var-free-code-camp; } -.@{fa-css-prefix}-telegram:before { content: @fa-var-telegram; } -.@{fa-css-prefix}-thermometer-4:before, -.@{fa-css-prefix}-thermometer:before, -.@{fa-css-prefix}-thermometer-full:before { content: @fa-var-thermometer-full; } -.@{fa-css-prefix}-thermometer-3:before, -.@{fa-css-prefix}-thermometer-three-quarters:before { content: @fa-var-thermometer-three-quarters; } -.@{fa-css-prefix}-thermometer-2:before, -.@{fa-css-prefix}-thermometer-half:before { content: @fa-var-thermometer-half; } -.@{fa-css-prefix}-thermometer-1:before, -.@{fa-css-prefix}-thermometer-quarter:before { content: @fa-var-thermometer-quarter; } -.@{fa-css-prefix}-thermometer-0:before, -.@{fa-css-prefix}-thermometer-empty:before { content: @fa-var-thermometer-empty; } -.@{fa-css-prefix}-shower:before { content: @fa-var-shower; } -.@{fa-css-prefix}-bathtub:before, -.@{fa-css-prefix}-s15:before, -.@{fa-css-prefix}-bath:before { content: @fa-var-bath; } -.@{fa-css-prefix}-podcast:before { content: @fa-var-podcast; } -.@{fa-css-prefix}-window-maximize:before { content: @fa-var-window-maximize; } -.@{fa-css-prefix}-window-minimize:before { content: @fa-var-window-minimize; } -.@{fa-css-prefix}-window-restore:before { content: @fa-var-window-restore; } -.@{fa-css-prefix}-times-rectangle:before, -.@{fa-css-prefix}-window-close:before { content: @fa-var-window-close; } -.@{fa-css-prefix}-times-rectangle-o:before, -.@{fa-css-prefix}-window-close-o:before { content: @fa-var-window-close-o; } -.@{fa-css-prefix}-bandcamp:before { content: @fa-var-bandcamp; } -.@{fa-css-prefix}-grav:before { content: @fa-var-grav; } -.@{fa-css-prefix}-etsy:before { content: @fa-var-etsy; } -.@{fa-css-prefix}-imdb:before { content: @fa-var-imdb; } -.@{fa-css-prefix}-ravelry:before { content: @fa-var-ravelry; } -.@{fa-css-prefix}-eercast:before { content: @fa-var-eercast; } -.@{fa-css-prefix}-microchip:before { content: @fa-var-microchip; } -.@{fa-css-prefix}-snowflake-o:before { content: @fa-var-snowflake-o; } -.@{fa-css-prefix}-superpowers:before { content: @fa-var-superpowers; } -.@{fa-css-prefix}-wpexplorer:before { content: @fa-var-wpexplorer; } -.@{fa-css-prefix}-meetup:before { content: @fa-var-meetup; } diff --git a/resources/public/font-awesome-4.7.0/less/larger.less b/resources/public/font-awesome-4.7.0/less/larger.less deleted file mode 100644 index c9d646770..000000000 --- a/resources/public/font-awesome-4.7.0/less/larger.less +++ /dev/null @@ -1,13 +0,0 @@ -// Icon Sizes -// ------------------------- - -/* makes the font 33% larger relative to the icon container */ -.@{fa-css-prefix}-lg { - font-size: (4em / 3); - line-height: (3em / 4); - vertical-align: -15%; -} -.@{fa-css-prefix}-2x { font-size: 2em; } -.@{fa-css-prefix}-3x { font-size: 3em; } -.@{fa-css-prefix}-4x { font-size: 4em; } -.@{fa-css-prefix}-5x { font-size: 5em; } diff --git a/resources/public/font-awesome-4.7.0/less/list.less b/resources/public/font-awesome-4.7.0/less/list.less deleted file mode 100644 index 0b440382f..000000000 --- a/resources/public/font-awesome-4.7.0/less/list.less +++ /dev/null @@ -1,19 +0,0 @@ -// List Icons -// ------------------------- - -.@{fa-css-prefix}-ul { - padding-left: 0; - margin-left: @fa-li-width; - list-style-type: none; - > li { position: relative; } -} -.@{fa-css-prefix}-li { - position: absolute; - left: -@fa-li-width; - width: @fa-li-width; - top: (2em / 14); - text-align: center; - &.@{fa-css-prefix}-lg { - left: (-@fa-li-width + (4em / 14)); - } -} diff --git a/resources/public/font-awesome-4.7.0/less/mixins.less b/resources/public/font-awesome-4.7.0/less/mixins.less deleted file mode 100644 index beef231d0..000000000 --- a/resources/public/font-awesome-4.7.0/less/mixins.less +++ /dev/null @@ -1,60 +0,0 @@ -// Mixins -// -------------------------- - -.fa-icon() { - display: inline-block; - font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration - font-size: inherit; // can't have font-size inherit on line above, so need to override - text-rendering: auto; // optimizelegibility throws things off #1094 - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -} - -.fa-icon-rotate(@degrees, @rotation) { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; - -webkit-transform: rotate(@degrees); - -ms-transform: rotate(@degrees); - transform: rotate(@degrees); -} - -.fa-icon-flip(@horiz, @vert, @rotation) { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; - -webkit-transform: scale(@horiz, @vert); - -ms-transform: scale(@horiz, @vert); - transform: scale(@horiz, @vert); -} - - -// Only display content to screen readers. A la Bootstrap 4. -// -// See: http://a11yproject.com/posts/how-to-hide-content/ - -.sr-only() { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0,0,0,0); - border: 0; -} - -// Use in conjunction with .sr-only to only display content when it's focused. -// -// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 -// -// Credit: HTML5 Boilerplate - -.sr-only-focusable() { - &:active, - &:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; - } -} diff --git a/resources/public/font-awesome-4.7.0/less/path.less b/resources/public/font-awesome-4.7.0/less/path.less deleted file mode 100644 index 835be41f8..000000000 --- a/resources/public/font-awesome-4.7.0/less/path.less +++ /dev/null @@ -1,15 +0,0 @@ -/* FONT PATH - * -------------------------- */ - -@font-face { - font-family: 'FontAwesome'; - src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); - src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), - url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), - url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), - url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), - url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); - // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts - font-weight: normal; - font-style: normal; -} diff --git a/resources/public/font-awesome-4.7.0/less/rotated-flipped.less b/resources/public/font-awesome-4.7.0/less/rotated-flipped.less deleted file mode 100644 index f6ba81475..000000000 --- a/resources/public/font-awesome-4.7.0/less/rotated-flipped.less +++ /dev/null @@ -1,20 +0,0 @@ -// Rotated & Flipped Icons -// ------------------------- - -.@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } -.@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } -.@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } - -.@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } -.@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } - -// Hook for IE8-9 -// ------------------------- - -:root .@{fa-css-prefix}-rotate-90, -:root .@{fa-css-prefix}-rotate-180, -:root .@{fa-css-prefix}-rotate-270, -:root .@{fa-css-prefix}-flip-horizontal, -:root .@{fa-css-prefix}-flip-vertical { - filter: none; -} diff --git a/resources/public/font-awesome-4.7.0/less/screen-reader.less b/resources/public/font-awesome-4.7.0/less/screen-reader.less deleted file mode 100644 index 11c188196..000000000 --- a/resources/public/font-awesome-4.7.0/less/screen-reader.less +++ /dev/null @@ -1,5 +0,0 @@ -// Screen Readers -// ------------------------- - -.sr-only { .sr-only(); } -.sr-only-focusable { .sr-only-focusable(); } diff --git a/resources/public/font-awesome-4.7.0/less/stacked.less b/resources/public/font-awesome-4.7.0/less/stacked.less deleted file mode 100644 index fc53fb0e7..000000000 --- a/resources/public/font-awesome-4.7.0/less/stacked.less +++ /dev/null @@ -1,20 +0,0 @@ -// Stacked Icons -// ------------------------- - -.@{fa-css-prefix}-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; -} -.@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; -} -.@{fa-css-prefix}-stack-1x { line-height: inherit; } -.@{fa-css-prefix}-stack-2x { font-size: 2em; } -.@{fa-css-prefix}-inverse { color: @fa-inverse; } diff --git a/resources/public/font-awesome-4.7.0/less/variables.less b/resources/public/font-awesome-4.7.0/less/variables.less deleted file mode 100644 index 7ddbbc011..000000000 --- a/resources/public/font-awesome-4.7.0/less/variables.less +++ /dev/null @@ -1,800 +0,0 @@ -// Variables -// -------------------------- - -@fa-font-path: "../fonts"; -@fa-font-size-base: 14px; -@fa-line-height-base: 1; -//@fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts"; // for referencing Bootstrap CDN font files directly -@fa-css-prefix: fa; -@fa-version: "4.7.0"; -@fa-border-color: #eee; -@fa-inverse: #fff; -@fa-li-width: (30em / 14); - -@fa-var-500px: "\f26e"; -@fa-var-address-book: "\f2b9"; -@fa-var-address-book-o: "\f2ba"; -@fa-var-address-card: "\f2bb"; -@fa-var-address-card-o: "\f2bc"; -@fa-var-adjust: "\f042"; -@fa-var-adn: "\f170"; -@fa-var-align-center: "\f037"; -@fa-var-align-justify: "\f039"; -@fa-var-align-left: "\f036"; -@fa-var-align-right: "\f038"; -@fa-var-amazon: "\f270"; -@fa-var-ambulance: "\f0f9"; -@fa-var-american-sign-language-interpreting: "\f2a3"; -@fa-var-anchor: "\f13d"; -@fa-var-android: "\f17b"; -@fa-var-angellist: "\f209"; -@fa-var-angle-double-down: "\f103"; -@fa-var-angle-double-left: "\f100"; -@fa-var-angle-double-right: "\f101"; -@fa-var-angle-double-up: "\f102"; -@fa-var-angle-down: "\f107"; -@fa-var-angle-left: "\f104"; -@fa-var-angle-right: "\f105"; -@fa-var-angle-up: "\f106"; -@fa-var-apple: "\f179"; -@fa-var-archive: "\f187"; -@fa-var-area-chart: "\f1fe"; -@fa-var-arrow-circle-down: "\f0ab"; -@fa-var-arrow-circle-left: "\f0a8"; -@fa-var-arrow-circle-o-down: "\f01a"; -@fa-var-arrow-circle-o-left: "\f190"; -@fa-var-arrow-circle-o-right: "\f18e"; -@fa-var-arrow-circle-o-up: "\f01b"; -@fa-var-arrow-circle-right: "\f0a9"; -@fa-var-arrow-circle-up: "\f0aa"; -@fa-var-arrow-down: "\f063"; -@fa-var-arrow-left: "\f060"; -@fa-var-arrow-right: "\f061"; -@fa-var-arrow-up: "\f062"; -@fa-var-arrows: "\f047"; -@fa-var-arrows-alt: "\f0b2"; -@fa-var-arrows-h: "\f07e"; -@fa-var-arrows-v: "\f07d"; -@fa-var-asl-interpreting: "\f2a3"; -@fa-var-assistive-listening-systems: "\f2a2"; -@fa-var-asterisk: "\f069"; -@fa-var-at: "\f1fa"; -@fa-var-audio-description: "\f29e"; -@fa-var-automobile: "\f1b9"; -@fa-var-backward: "\f04a"; -@fa-var-balance-scale: "\f24e"; -@fa-var-ban: "\f05e"; -@fa-var-bandcamp: "\f2d5"; -@fa-var-bank: "\f19c"; -@fa-var-bar-chart: "\f080"; -@fa-var-bar-chart-o: "\f080"; -@fa-var-barcode: "\f02a"; -@fa-var-bars: "\f0c9"; -@fa-var-bath: "\f2cd"; -@fa-var-bathtub: "\f2cd"; -@fa-var-battery: "\f240"; -@fa-var-battery-0: "\f244"; -@fa-var-battery-1: "\f243"; -@fa-var-battery-2: "\f242"; -@fa-var-battery-3: "\f241"; -@fa-var-battery-4: "\f240"; -@fa-var-battery-empty: "\f244"; -@fa-var-battery-full: "\f240"; -@fa-var-battery-half: "\f242"; -@fa-var-battery-quarter: "\f243"; -@fa-var-battery-three-quarters: "\f241"; -@fa-var-bed: "\f236"; -@fa-var-beer: "\f0fc"; -@fa-var-behance: "\f1b4"; -@fa-var-behance-square: "\f1b5"; -@fa-var-bell: "\f0f3"; -@fa-var-bell-o: "\f0a2"; -@fa-var-bell-slash: "\f1f6"; -@fa-var-bell-slash-o: "\f1f7"; -@fa-var-bicycle: "\f206"; -@fa-var-binoculars: "\f1e5"; -@fa-var-birthday-cake: "\f1fd"; -@fa-var-bitbucket: "\f171"; -@fa-var-bitbucket-square: "\f172"; -@fa-var-bitcoin: "\f15a"; -@fa-var-black-tie: "\f27e"; -@fa-var-blind: "\f29d"; -@fa-var-bluetooth: "\f293"; -@fa-var-bluetooth-b: "\f294"; -@fa-var-bold: "\f032"; -@fa-var-bolt: "\f0e7"; -@fa-var-bomb: "\f1e2"; -@fa-var-book: "\f02d"; -@fa-var-bookmark: "\f02e"; -@fa-var-bookmark-o: "\f097"; -@fa-var-braille: "\f2a1"; -@fa-var-briefcase: "\f0b1"; -@fa-var-btc: "\f15a"; -@fa-var-bug: "\f188"; -@fa-var-building: "\f1ad"; -@fa-var-building-o: "\f0f7"; -@fa-var-bullhorn: "\f0a1"; -@fa-var-bullseye: "\f140"; -@fa-var-bus: "\f207"; -@fa-var-buysellads: "\f20d"; -@fa-var-cab: "\f1ba"; -@fa-var-calculator: "\f1ec"; -@fa-var-calendar: "\f073"; -@fa-var-calendar-check-o: "\f274"; -@fa-var-calendar-minus-o: "\f272"; -@fa-var-calendar-o: "\f133"; -@fa-var-calendar-plus-o: "\f271"; -@fa-var-calendar-times-o: "\f273"; -@fa-var-camera: "\f030"; -@fa-var-camera-retro: "\f083"; -@fa-var-car: "\f1b9"; -@fa-var-caret-down: "\f0d7"; -@fa-var-caret-left: "\f0d9"; -@fa-var-caret-right: "\f0da"; -@fa-var-caret-square-o-down: "\f150"; -@fa-var-caret-square-o-left: "\f191"; -@fa-var-caret-square-o-right: "\f152"; -@fa-var-caret-square-o-up: "\f151"; -@fa-var-caret-up: "\f0d8"; -@fa-var-cart-arrow-down: "\f218"; -@fa-var-cart-plus: "\f217"; -@fa-var-cc: "\f20a"; -@fa-var-cc-amex: "\f1f3"; -@fa-var-cc-diners-club: "\f24c"; -@fa-var-cc-discover: "\f1f2"; -@fa-var-cc-jcb: "\f24b"; -@fa-var-cc-mastercard: "\f1f1"; -@fa-var-cc-paypal: "\f1f4"; -@fa-var-cc-stripe: "\f1f5"; -@fa-var-cc-visa: "\f1f0"; -@fa-var-certificate: "\f0a3"; -@fa-var-chain: "\f0c1"; -@fa-var-chain-broken: "\f127"; -@fa-var-check: "\f00c"; -@fa-var-check-circle: "\f058"; -@fa-var-check-circle-o: "\f05d"; -@fa-var-check-square: "\f14a"; -@fa-var-check-square-o: "\f046"; -@fa-var-chevron-circle-down: "\f13a"; -@fa-var-chevron-circle-left: "\f137"; -@fa-var-chevron-circle-right: "\f138"; -@fa-var-chevron-circle-up: "\f139"; -@fa-var-chevron-down: "\f078"; -@fa-var-chevron-left: "\f053"; -@fa-var-chevron-right: "\f054"; -@fa-var-chevron-up: "\f077"; -@fa-var-child: "\f1ae"; -@fa-var-chrome: "\f268"; -@fa-var-circle: "\f111"; -@fa-var-circle-o: "\f10c"; -@fa-var-circle-o-notch: "\f1ce"; -@fa-var-circle-thin: "\f1db"; -@fa-var-clipboard: "\f0ea"; -@fa-var-clock-o: "\f017"; -@fa-var-clone: "\f24d"; -@fa-var-close: "\f00d"; -@fa-var-cloud: "\f0c2"; -@fa-var-cloud-download: "\f0ed"; -@fa-var-cloud-upload: "\f0ee"; -@fa-var-cny: "\f157"; -@fa-var-code: "\f121"; -@fa-var-code-fork: "\f126"; -@fa-var-codepen: "\f1cb"; -@fa-var-codiepie: "\f284"; -@fa-var-coffee: "\f0f4"; -@fa-var-cog: "\f013"; -@fa-var-cogs: "\f085"; -@fa-var-columns: "\f0db"; -@fa-var-comment: "\f075"; -@fa-var-comment-o: "\f0e5"; -@fa-var-commenting: "\f27a"; -@fa-var-commenting-o: "\f27b"; -@fa-var-comments: "\f086"; -@fa-var-comments-o: "\f0e6"; -@fa-var-compass: "\f14e"; -@fa-var-compress: "\f066"; -@fa-var-connectdevelop: "\f20e"; -@fa-var-contao: "\f26d"; -@fa-var-copy: "\f0c5"; -@fa-var-copyright: "\f1f9"; -@fa-var-creative-commons: "\f25e"; -@fa-var-credit-card: "\f09d"; -@fa-var-credit-card-alt: "\f283"; -@fa-var-crop: "\f125"; -@fa-var-crosshairs: "\f05b"; -@fa-var-css3: "\f13c"; -@fa-var-cube: "\f1b2"; -@fa-var-cubes: "\f1b3"; -@fa-var-cut: "\f0c4"; -@fa-var-cutlery: "\f0f5"; -@fa-var-dashboard: "\f0e4"; -@fa-var-dashcube: "\f210"; -@fa-var-database: "\f1c0"; -@fa-var-deaf: "\f2a4"; -@fa-var-deafness: "\f2a4"; -@fa-var-dedent: "\f03b"; -@fa-var-delicious: "\f1a5"; -@fa-var-desktop: "\f108"; -@fa-var-deviantart: "\f1bd"; -@fa-var-diamond: "\f219"; -@fa-var-digg: "\f1a6"; -@fa-var-dollar: "\f155"; -@fa-var-dot-circle-o: "\f192"; -@fa-var-download: "\f019"; -@fa-var-dribbble: "\f17d"; -@fa-var-drivers-license: "\f2c2"; -@fa-var-drivers-license-o: "\f2c3"; -@fa-var-dropbox: "\f16b"; -@fa-var-drupal: "\f1a9"; -@fa-var-edge: "\f282"; -@fa-var-edit: "\f044"; -@fa-var-eercast: "\f2da"; -@fa-var-eject: "\f052"; -@fa-var-ellipsis-h: "\f141"; -@fa-var-ellipsis-v: "\f142"; -@fa-var-empire: "\f1d1"; -@fa-var-envelope: "\f0e0"; -@fa-var-envelope-o: "\f003"; -@fa-var-envelope-open: "\f2b6"; -@fa-var-envelope-open-o: "\f2b7"; -@fa-var-envelope-square: "\f199"; -@fa-var-envira: "\f299"; -@fa-var-eraser: "\f12d"; -@fa-var-etsy: "\f2d7"; -@fa-var-eur: "\f153"; -@fa-var-euro: "\f153"; -@fa-var-exchange: "\f0ec"; -@fa-var-exclamation: "\f12a"; -@fa-var-exclamation-circle: "\f06a"; -@fa-var-exclamation-triangle: "\f071"; -@fa-var-expand: "\f065"; -@fa-var-expeditedssl: "\f23e"; -@fa-var-external-link: "\f08e"; -@fa-var-external-link-square: "\f14c"; -@fa-var-eye: "\f06e"; -@fa-var-eye-slash: "\f070"; -@fa-var-eyedropper: "\f1fb"; -@fa-var-fa: "\f2b4"; -@fa-var-facebook: "\f09a"; -@fa-var-facebook-f: "\f09a"; -@fa-var-facebook-official: "\f230"; -@fa-var-facebook-square: "\f082"; -@fa-var-fast-backward: "\f049"; -@fa-var-fast-forward: "\f050"; -@fa-var-fax: "\f1ac"; -@fa-var-feed: "\f09e"; -@fa-var-female: "\f182"; -@fa-var-fighter-jet: "\f0fb"; -@fa-var-file: "\f15b"; -@fa-var-file-archive-o: "\f1c6"; -@fa-var-file-audio-o: "\f1c7"; -@fa-var-file-code-o: "\f1c9"; -@fa-var-file-excel-o: "\f1c3"; -@fa-var-file-image-o: "\f1c5"; -@fa-var-file-movie-o: "\f1c8"; -@fa-var-file-o: "\f016"; -@fa-var-file-pdf-o: "\f1c1"; -@fa-var-file-photo-o: "\f1c5"; -@fa-var-file-picture-o: "\f1c5"; -@fa-var-file-powerpoint-o: "\f1c4"; -@fa-var-file-sound-o: "\f1c7"; -@fa-var-file-text: "\f15c"; -@fa-var-file-text-o: "\f0f6"; -@fa-var-file-video-o: "\f1c8"; -@fa-var-file-word-o: "\f1c2"; -@fa-var-file-zip-o: "\f1c6"; -@fa-var-files-o: "\f0c5"; -@fa-var-film: "\f008"; -@fa-var-filter: "\f0b0"; -@fa-var-fire: "\f06d"; -@fa-var-fire-extinguisher: "\f134"; -@fa-var-firefox: "\f269"; -@fa-var-first-order: "\f2b0"; -@fa-var-flag: "\f024"; -@fa-var-flag-checkered: "\f11e"; -@fa-var-flag-o: "\f11d"; -@fa-var-flash: "\f0e7"; -@fa-var-flask: "\f0c3"; -@fa-var-flickr: "\f16e"; -@fa-var-floppy-o: "\f0c7"; -@fa-var-folder: "\f07b"; -@fa-var-folder-o: "\f114"; -@fa-var-folder-open: "\f07c"; -@fa-var-folder-open-o: "\f115"; -@fa-var-font: "\f031"; -@fa-var-font-awesome: "\f2b4"; -@fa-var-fonticons: "\f280"; -@fa-var-fort-awesome: "\f286"; -@fa-var-forumbee: "\f211"; -@fa-var-forward: "\f04e"; -@fa-var-foursquare: "\f180"; -@fa-var-free-code-camp: "\f2c5"; -@fa-var-frown-o: "\f119"; -@fa-var-futbol-o: "\f1e3"; -@fa-var-gamepad: "\f11b"; -@fa-var-gavel: "\f0e3"; -@fa-var-gbp: "\f154"; -@fa-var-ge: "\f1d1"; -@fa-var-gear: "\f013"; -@fa-var-gears: "\f085"; -@fa-var-genderless: "\f22d"; -@fa-var-get-pocket: "\f265"; -@fa-var-gg: "\f260"; -@fa-var-gg-circle: "\f261"; -@fa-var-gift: "\f06b"; -@fa-var-git: "\f1d3"; -@fa-var-git-square: "\f1d2"; -@fa-var-github: "\f09b"; -@fa-var-github-alt: "\f113"; -@fa-var-github-square: "\f092"; -@fa-var-gitlab: "\f296"; -@fa-var-gittip: "\f184"; -@fa-var-glass: "\f000"; -@fa-var-glide: "\f2a5"; -@fa-var-glide-g: "\f2a6"; -@fa-var-globe: "\f0ac"; -@fa-var-google: "\f1a0"; -@fa-var-google-plus: "\f0d5"; -@fa-var-google-plus-circle: "\f2b3"; -@fa-var-google-plus-official: "\f2b3"; -@fa-var-google-plus-square: "\f0d4"; -@fa-var-google-wallet: "\f1ee"; -@fa-var-graduation-cap: "\f19d"; -@fa-var-gratipay: "\f184"; -@fa-var-grav: "\f2d6"; -@fa-var-group: "\f0c0"; -@fa-var-h-square: "\f0fd"; -@fa-var-hacker-news: "\f1d4"; -@fa-var-hand-grab-o: "\f255"; -@fa-var-hand-lizard-o: "\f258"; -@fa-var-hand-o-down: "\f0a7"; -@fa-var-hand-o-left: "\f0a5"; -@fa-var-hand-o-right: "\f0a4"; -@fa-var-hand-o-up: "\f0a6"; -@fa-var-hand-paper-o: "\f256"; -@fa-var-hand-peace-o: "\f25b"; -@fa-var-hand-pointer-o: "\f25a"; -@fa-var-hand-rock-o: "\f255"; -@fa-var-hand-scissors-o: "\f257"; -@fa-var-hand-spock-o: "\f259"; -@fa-var-hand-stop-o: "\f256"; -@fa-var-handshake-o: "\f2b5"; -@fa-var-hard-of-hearing: "\f2a4"; -@fa-var-hashtag: "\f292"; -@fa-var-hdd-o: "\f0a0"; -@fa-var-header: "\f1dc"; -@fa-var-headphones: "\f025"; -@fa-var-heart: "\f004"; -@fa-var-heart-o: "\f08a"; -@fa-var-heartbeat: "\f21e"; -@fa-var-history: "\f1da"; -@fa-var-home: "\f015"; -@fa-var-hospital-o: "\f0f8"; -@fa-var-hotel: "\f236"; -@fa-var-hourglass: "\f254"; -@fa-var-hourglass-1: "\f251"; -@fa-var-hourglass-2: "\f252"; -@fa-var-hourglass-3: "\f253"; -@fa-var-hourglass-end: "\f253"; -@fa-var-hourglass-half: "\f252"; -@fa-var-hourglass-o: "\f250"; -@fa-var-hourglass-start: "\f251"; -@fa-var-houzz: "\f27c"; -@fa-var-html5: "\f13b"; -@fa-var-i-cursor: "\f246"; -@fa-var-id-badge: "\f2c1"; -@fa-var-id-card: "\f2c2"; -@fa-var-id-card-o: "\f2c3"; -@fa-var-ils: "\f20b"; -@fa-var-image: "\f03e"; -@fa-var-imdb: "\f2d8"; -@fa-var-inbox: "\f01c"; -@fa-var-indent: "\f03c"; -@fa-var-industry: "\f275"; -@fa-var-info: "\f129"; -@fa-var-info-circle: "\f05a"; -@fa-var-inr: "\f156"; -@fa-var-instagram: "\f16d"; -@fa-var-institution: "\f19c"; -@fa-var-internet-explorer: "\f26b"; -@fa-var-intersex: "\f224"; -@fa-var-ioxhost: "\f208"; -@fa-var-italic: "\f033"; -@fa-var-joomla: "\f1aa"; -@fa-var-jpy: "\f157"; -@fa-var-jsfiddle: "\f1cc"; -@fa-var-key: "\f084"; -@fa-var-keyboard-o: "\f11c"; -@fa-var-krw: "\f159"; -@fa-var-language: "\f1ab"; -@fa-var-laptop: "\f109"; -@fa-var-lastfm: "\f202"; -@fa-var-lastfm-square: "\f203"; -@fa-var-leaf: "\f06c"; -@fa-var-leanpub: "\f212"; -@fa-var-legal: "\f0e3"; -@fa-var-lemon-o: "\f094"; -@fa-var-level-down: "\f149"; -@fa-var-level-up: "\f148"; -@fa-var-life-bouy: "\f1cd"; -@fa-var-life-buoy: "\f1cd"; -@fa-var-life-ring: "\f1cd"; -@fa-var-life-saver: "\f1cd"; -@fa-var-lightbulb-o: "\f0eb"; -@fa-var-line-chart: "\f201"; -@fa-var-link: "\f0c1"; -@fa-var-linkedin: "\f0e1"; -@fa-var-linkedin-square: "\f08c"; -@fa-var-linode: "\f2b8"; -@fa-var-linux: "\f17c"; -@fa-var-list: "\f03a"; -@fa-var-list-alt: "\f022"; -@fa-var-list-ol: "\f0cb"; -@fa-var-list-ul: "\f0ca"; -@fa-var-location-arrow: "\f124"; -@fa-var-lock: "\f023"; -@fa-var-long-arrow-down: "\f175"; -@fa-var-long-arrow-left: "\f177"; -@fa-var-long-arrow-right: "\f178"; -@fa-var-long-arrow-up: "\f176"; -@fa-var-low-vision: "\f2a8"; -@fa-var-magic: "\f0d0"; -@fa-var-magnet: "\f076"; -@fa-var-mail-forward: "\f064"; -@fa-var-mail-reply: "\f112"; -@fa-var-mail-reply-all: "\f122"; -@fa-var-male: "\f183"; -@fa-var-map: "\f279"; -@fa-var-map-marker: "\f041"; -@fa-var-map-o: "\f278"; -@fa-var-map-pin: "\f276"; -@fa-var-map-signs: "\f277"; -@fa-var-mars: "\f222"; -@fa-var-mars-double: "\f227"; -@fa-var-mars-stroke: "\f229"; -@fa-var-mars-stroke-h: "\f22b"; -@fa-var-mars-stroke-v: "\f22a"; -@fa-var-maxcdn: "\f136"; -@fa-var-meanpath: "\f20c"; -@fa-var-medium: "\f23a"; -@fa-var-medkit: "\f0fa"; -@fa-var-meetup: "\f2e0"; -@fa-var-meh-o: "\f11a"; -@fa-var-mercury: "\f223"; -@fa-var-microchip: "\f2db"; -@fa-var-microphone: "\f130"; -@fa-var-microphone-slash: "\f131"; -@fa-var-minus: "\f068"; -@fa-var-minus-circle: "\f056"; -@fa-var-minus-square: "\f146"; -@fa-var-minus-square-o: "\f147"; -@fa-var-mixcloud: "\f289"; -@fa-var-mobile: "\f10b"; -@fa-var-mobile-phone: "\f10b"; -@fa-var-modx: "\f285"; -@fa-var-money: "\f0d6"; -@fa-var-moon-o: "\f186"; -@fa-var-mortar-board: "\f19d"; -@fa-var-motorcycle: "\f21c"; -@fa-var-mouse-pointer: "\f245"; -@fa-var-music: "\f001"; -@fa-var-navicon: "\f0c9"; -@fa-var-neuter: "\f22c"; -@fa-var-newspaper-o: "\f1ea"; -@fa-var-object-group: "\f247"; -@fa-var-object-ungroup: "\f248"; -@fa-var-odnoklassniki: "\f263"; -@fa-var-odnoklassniki-square: "\f264"; -@fa-var-opencart: "\f23d"; -@fa-var-openid: "\f19b"; -@fa-var-opera: "\f26a"; -@fa-var-optin-monster: "\f23c"; -@fa-var-outdent: "\f03b"; -@fa-var-pagelines: "\f18c"; -@fa-var-paint-brush: "\f1fc"; -@fa-var-paper-plane: "\f1d8"; -@fa-var-paper-plane-o: "\f1d9"; -@fa-var-paperclip: "\f0c6"; -@fa-var-paragraph: "\f1dd"; -@fa-var-paste: "\f0ea"; -@fa-var-pause: "\f04c"; -@fa-var-pause-circle: "\f28b"; -@fa-var-pause-circle-o: "\f28c"; -@fa-var-paw: "\f1b0"; -@fa-var-paypal: "\f1ed"; -@fa-var-pencil: "\f040"; -@fa-var-pencil-square: "\f14b"; -@fa-var-pencil-square-o: "\f044"; -@fa-var-percent: "\f295"; -@fa-var-phone: "\f095"; -@fa-var-phone-square: "\f098"; -@fa-var-photo: "\f03e"; -@fa-var-picture-o: "\f03e"; -@fa-var-pie-chart: "\f200"; -@fa-var-pied-piper: "\f2ae"; -@fa-var-pied-piper-alt: "\f1a8"; -@fa-var-pied-piper-pp: "\f1a7"; -@fa-var-pinterest: "\f0d2"; -@fa-var-pinterest-p: "\f231"; -@fa-var-pinterest-square: "\f0d3"; -@fa-var-plane: "\f072"; -@fa-var-play: "\f04b"; -@fa-var-play-circle: "\f144"; -@fa-var-play-circle-o: "\f01d"; -@fa-var-plug: "\f1e6"; -@fa-var-plus: "\f067"; -@fa-var-plus-circle: "\f055"; -@fa-var-plus-square: "\f0fe"; -@fa-var-plus-square-o: "\f196"; -@fa-var-podcast: "\f2ce"; -@fa-var-power-off: "\f011"; -@fa-var-print: "\f02f"; -@fa-var-product-hunt: "\f288"; -@fa-var-puzzle-piece: "\f12e"; -@fa-var-qq: "\f1d6"; -@fa-var-qrcode: "\f029"; -@fa-var-question: "\f128"; -@fa-var-question-circle: "\f059"; -@fa-var-question-circle-o: "\f29c"; -@fa-var-quora: "\f2c4"; -@fa-var-quote-left: "\f10d"; -@fa-var-quote-right: "\f10e"; -@fa-var-ra: "\f1d0"; -@fa-var-random: "\f074"; -@fa-var-ravelry: "\f2d9"; -@fa-var-rebel: "\f1d0"; -@fa-var-recycle: "\f1b8"; -@fa-var-reddit: "\f1a1"; -@fa-var-reddit-alien: "\f281"; -@fa-var-reddit-square: "\f1a2"; -@fa-var-refresh: "\f021"; -@fa-var-registered: "\f25d"; -@fa-var-remove: "\f00d"; -@fa-var-renren: "\f18b"; -@fa-var-reorder: "\f0c9"; -@fa-var-repeat: "\f01e"; -@fa-var-reply: "\f112"; -@fa-var-reply-all: "\f122"; -@fa-var-resistance: "\f1d0"; -@fa-var-retweet: "\f079"; -@fa-var-rmb: "\f157"; -@fa-var-road: "\f018"; -@fa-var-rocket: "\f135"; -@fa-var-rotate-left: "\f0e2"; -@fa-var-rotate-right: "\f01e"; -@fa-var-rouble: "\f158"; -@fa-var-rss: "\f09e"; -@fa-var-rss-square: "\f143"; -@fa-var-rub: "\f158"; -@fa-var-ruble: "\f158"; -@fa-var-rupee: "\f156"; -@fa-var-s15: "\f2cd"; -@fa-var-safari: "\f267"; -@fa-var-save: "\f0c7"; -@fa-var-scissors: "\f0c4"; -@fa-var-scribd: "\f28a"; -@fa-var-search: "\f002"; -@fa-var-search-minus: "\f010"; -@fa-var-search-plus: "\f00e"; -@fa-var-sellsy: "\f213"; -@fa-var-send: "\f1d8"; -@fa-var-send-o: "\f1d9"; -@fa-var-server: "\f233"; -@fa-var-share: "\f064"; -@fa-var-share-alt: "\f1e0"; -@fa-var-share-alt-square: "\f1e1"; -@fa-var-share-square: "\f14d"; -@fa-var-share-square-o: "\f045"; -@fa-var-shekel: "\f20b"; -@fa-var-sheqel: "\f20b"; -@fa-var-shield: "\f132"; -@fa-var-ship: "\f21a"; -@fa-var-shirtsinbulk: "\f214"; -@fa-var-shopping-bag: "\f290"; -@fa-var-shopping-basket: "\f291"; -@fa-var-shopping-cart: "\f07a"; -@fa-var-shower: "\f2cc"; -@fa-var-sign-in: "\f090"; -@fa-var-sign-language: "\f2a7"; -@fa-var-sign-out: "\f08b"; -@fa-var-signal: "\f012"; -@fa-var-signing: "\f2a7"; -@fa-var-simplybuilt: "\f215"; -@fa-var-sitemap: "\f0e8"; -@fa-var-skyatlas: "\f216"; -@fa-var-skype: "\f17e"; -@fa-var-slack: "\f198"; -@fa-var-sliders: "\f1de"; -@fa-var-slideshare: "\f1e7"; -@fa-var-smile-o: "\f118"; -@fa-var-snapchat: "\f2ab"; -@fa-var-snapchat-ghost: "\f2ac"; -@fa-var-snapchat-square: "\f2ad"; -@fa-var-snowflake-o: "\f2dc"; -@fa-var-soccer-ball-o: "\f1e3"; -@fa-var-sort: "\f0dc"; -@fa-var-sort-alpha-asc: "\f15d"; -@fa-var-sort-alpha-desc: "\f15e"; -@fa-var-sort-amount-asc: "\f160"; -@fa-var-sort-amount-desc: "\f161"; -@fa-var-sort-asc: "\f0de"; -@fa-var-sort-desc: "\f0dd"; -@fa-var-sort-down: "\f0dd"; -@fa-var-sort-numeric-asc: "\f162"; -@fa-var-sort-numeric-desc: "\f163"; -@fa-var-sort-up: "\f0de"; -@fa-var-soundcloud: "\f1be"; -@fa-var-space-shuttle: "\f197"; -@fa-var-spinner: "\f110"; -@fa-var-spoon: "\f1b1"; -@fa-var-spotify: "\f1bc"; -@fa-var-square: "\f0c8"; -@fa-var-square-o: "\f096"; -@fa-var-stack-exchange: "\f18d"; -@fa-var-stack-overflow: "\f16c"; -@fa-var-star: "\f005"; -@fa-var-star-half: "\f089"; -@fa-var-star-half-empty: "\f123"; -@fa-var-star-half-full: "\f123"; -@fa-var-star-half-o: "\f123"; -@fa-var-star-o: "\f006"; -@fa-var-steam: "\f1b6"; -@fa-var-steam-square: "\f1b7"; -@fa-var-step-backward: "\f048"; -@fa-var-step-forward: "\f051"; -@fa-var-stethoscope: "\f0f1"; -@fa-var-sticky-note: "\f249"; -@fa-var-sticky-note-o: "\f24a"; -@fa-var-stop: "\f04d"; -@fa-var-stop-circle: "\f28d"; -@fa-var-stop-circle-o: "\f28e"; -@fa-var-street-view: "\f21d"; -@fa-var-strikethrough: "\f0cc"; -@fa-var-stumbleupon: "\f1a4"; -@fa-var-stumbleupon-circle: "\f1a3"; -@fa-var-subscript: "\f12c"; -@fa-var-subway: "\f239"; -@fa-var-suitcase: "\f0f2"; -@fa-var-sun-o: "\f185"; -@fa-var-superpowers: "\f2dd"; -@fa-var-superscript: "\f12b"; -@fa-var-support: "\f1cd"; -@fa-var-table: "\f0ce"; -@fa-var-tablet: "\f10a"; -@fa-var-tachometer: "\f0e4"; -@fa-var-tag: "\f02b"; -@fa-var-tags: "\f02c"; -@fa-var-tasks: "\f0ae"; -@fa-var-taxi: "\f1ba"; -@fa-var-telegram: "\f2c6"; -@fa-var-television: "\f26c"; -@fa-var-tencent-weibo: "\f1d5"; -@fa-var-terminal: "\f120"; -@fa-var-text-height: "\f034"; -@fa-var-text-width: "\f035"; -@fa-var-th: "\f00a"; -@fa-var-th-large: "\f009"; -@fa-var-th-list: "\f00b"; -@fa-var-themeisle: "\f2b2"; -@fa-var-thermometer: "\f2c7"; -@fa-var-thermometer-0: "\f2cb"; -@fa-var-thermometer-1: "\f2ca"; -@fa-var-thermometer-2: "\f2c9"; -@fa-var-thermometer-3: "\f2c8"; -@fa-var-thermometer-4: "\f2c7"; -@fa-var-thermometer-empty: "\f2cb"; -@fa-var-thermometer-full: "\f2c7"; -@fa-var-thermometer-half: "\f2c9"; -@fa-var-thermometer-quarter: "\f2ca"; -@fa-var-thermometer-three-quarters: "\f2c8"; -@fa-var-thumb-tack: "\f08d"; -@fa-var-thumbs-down: "\f165"; -@fa-var-thumbs-o-down: "\f088"; -@fa-var-thumbs-o-up: "\f087"; -@fa-var-thumbs-up: "\f164"; -@fa-var-ticket: "\f145"; -@fa-var-times: "\f00d"; -@fa-var-times-circle: "\f057"; -@fa-var-times-circle-o: "\f05c"; -@fa-var-times-rectangle: "\f2d3"; -@fa-var-times-rectangle-o: "\f2d4"; -@fa-var-tint: "\f043"; -@fa-var-toggle-down: "\f150"; -@fa-var-toggle-left: "\f191"; -@fa-var-toggle-off: "\f204"; -@fa-var-toggle-on: "\f205"; -@fa-var-toggle-right: "\f152"; -@fa-var-toggle-up: "\f151"; -@fa-var-trademark: "\f25c"; -@fa-var-train: "\f238"; -@fa-var-transgender: "\f224"; -@fa-var-transgender-alt: "\f225"; -@fa-var-trash: "\f1f8"; -@fa-var-trash-o: "\f014"; -@fa-var-tree: "\f1bb"; -@fa-var-trello: "\f181"; -@fa-var-tripadvisor: "\f262"; -@fa-var-trophy: "\f091"; -@fa-var-truck: "\f0d1"; -@fa-var-try: "\f195"; -@fa-var-tty: "\f1e4"; -@fa-var-tumblr: "\f173"; -@fa-var-tumblr-square: "\f174"; -@fa-var-turkish-lira: "\f195"; -@fa-var-tv: "\f26c"; -@fa-var-twitch: "\f1e8"; -@fa-var-twitter: "\f099"; -@fa-var-twitter-square: "\f081"; -@fa-var-umbrella: "\f0e9"; -@fa-var-underline: "\f0cd"; -@fa-var-undo: "\f0e2"; -@fa-var-universal-access: "\f29a"; -@fa-var-university: "\f19c"; -@fa-var-unlink: "\f127"; -@fa-var-unlock: "\f09c"; -@fa-var-unlock-alt: "\f13e"; -@fa-var-unsorted: "\f0dc"; -@fa-var-upload: "\f093"; -@fa-var-usb: "\f287"; -@fa-var-usd: "\f155"; -@fa-var-user: "\f007"; -@fa-var-user-circle: "\f2bd"; -@fa-var-user-circle-o: "\f2be"; -@fa-var-user-md: "\f0f0"; -@fa-var-user-o: "\f2c0"; -@fa-var-user-plus: "\f234"; -@fa-var-user-secret: "\f21b"; -@fa-var-user-times: "\f235"; -@fa-var-users: "\f0c0"; -@fa-var-vcard: "\f2bb"; -@fa-var-vcard-o: "\f2bc"; -@fa-var-venus: "\f221"; -@fa-var-venus-double: "\f226"; -@fa-var-venus-mars: "\f228"; -@fa-var-viacoin: "\f237"; -@fa-var-viadeo: "\f2a9"; -@fa-var-viadeo-square: "\f2aa"; -@fa-var-video-camera: "\f03d"; -@fa-var-vimeo: "\f27d"; -@fa-var-vimeo-square: "\f194"; -@fa-var-vine: "\f1ca"; -@fa-var-vk: "\f189"; -@fa-var-volume-control-phone: "\f2a0"; -@fa-var-volume-down: "\f027"; -@fa-var-volume-off: "\f026"; -@fa-var-volume-up: "\f028"; -@fa-var-warning: "\f071"; -@fa-var-wechat: "\f1d7"; -@fa-var-weibo: "\f18a"; -@fa-var-weixin: "\f1d7"; -@fa-var-whatsapp: "\f232"; -@fa-var-wheelchair: "\f193"; -@fa-var-wheelchair-alt: "\f29b"; -@fa-var-wifi: "\f1eb"; -@fa-var-wikipedia-w: "\f266"; -@fa-var-window-close: "\f2d3"; -@fa-var-window-close-o: "\f2d4"; -@fa-var-window-maximize: "\f2d0"; -@fa-var-window-minimize: "\f2d1"; -@fa-var-window-restore: "\f2d2"; -@fa-var-windows: "\f17a"; -@fa-var-won: "\f159"; -@fa-var-wordpress: "\f19a"; -@fa-var-wpbeginner: "\f297"; -@fa-var-wpexplorer: "\f2de"; -@fa-var-wpforms: "\f298"; -@fa-var-wrench: "\f0ad"; -@fa-var-xing: "\f168"; -@fa-var-xing-square: "\f169"; -@fa-var-y-combinator: "\f23b"; -@fa-var-y-combinator-square: "\f1d4"; -@fa-var-yahoo: "\f19e"; -@fa-var-yc: "\f23b"; -@fa-var-yc-square: "\f1d4"; -@fa-var-yelp: "\f1e9"; -@fa-var-yen: "\f157"; -@fa-var-yoast: "\f2b1"; -@fa-var-youtube: "\f167"; -@fa-var-youtube-play: "\f16a"; -@fa-var-youtube-square: "\f166"; - diff --git a/resources/public/font-awesome-4.7.0/scss/_animated.scss b/resources/public/font-awesome-4.7.0/scss/_animated.scss deleted file mode 100644 index 8a020dbff..000000000 --- a/resources/public/font-awesome-4.7.0/scss/_animated.scss +++ /dev/null @@ -1,34 +0,0 @@ -// Spinning Icons -// -------------------------- - -.#{$fa-css-prefix}-spin { - -webkit-animation: fa-spin 2s infinite linear; - animation: fa-spin 2s infinite linear; -} - -.#{$fa-css-prefix}-pulse { - -webkit-animation: fa-spin 1s infinite steps(8); - animation: fa-spin 1s infinite steps(8); -} - -@-webkit-keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} - -@keyframes fa-spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} diff --git a/resources/public/font-awesome-4.7.0/scss/_bordered-pulled.scss b/resources/public/font-awesome-4.7.0/scss/_bordered-pulled.scss deleted file mode 100644 index d4b85a02f..000000000 --- a/resources/public/font-awesome-4.7.0/scss/_bordered-pulled.scss +++ /dev/null @@ -1,25 +0,0 @@ -// Bordered & Pulled -// ------------------------- - -.#{$fa-css-prefix}-border { - padding: .2em .25em .15em; - border: solid .08em $fa-border-color; - border-radius: .1em; -} - -.#{$fa-css-prefix}-pull-left { float: left; } -.#{$fa-css-prefix}-pull-right { float: right; } - -.#{$fa-css-prefix} { - &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } - &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } -} - -/* Deprecated as of 4.4.0 */ -.pull-right { float: right; } -.pull-left { float: left; } - -.#{$fa-css-prefix} { - &.pull-left { margin-right: .3em; } - &.pull-right { margin-left: .3em; } -} diff --git a/resources/public/font-awesome-4.7.0/scss/_core.scss b/resources/public/font-awesome-4.7.0/scss/_core.scss deleted file mode 100644 index 7425ef85f..000000000 --- a/resources/public/font-awesome-4.7.0/scss/_core.scss +++ /dev/null @@ -1,12 +0,0 @@ -// Base Class Definition -// ------------------------- - -.#{$fa-css-prefix} { - display: inline-block; - font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration - font-size: inherit; // can't have font-size inherit on line above, so need to override - text-rendering: auto; // optimizelegibility throws things off #1094 - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -} diff --git a/resources/public/font-awesome-4.7.0/scss/_fixed-width.scss b/resources/public/font-awesome-4.7.0/scss/_fixed-width.scss deleted file mode 100644 index b221c9813..000000000 --- a/resources/public/font-awesome-4.7.0/scss/_fixed-width.scss +++ /dev/null @@ -1,6 +0,0 @@ -// Fixed Width Icons -// ------------------------- -.#{$fa-css-prefix}-fw { - width: (18em / 14); - text-align: center; -} diff --git a/resources/public/font-awesome-4.7.0/scss/_icons.scss b/resources/public/font-awesome-4.7.0/scss/_icons.scss deleted file mode 100644 index e63e702c4..000000000 --- a/resources/public/font-awesome-4.7.0/scss/_icons.scss +++ /dev/null @@ -1,789 +0,0 @@ -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ - -.#{$fa-css-prefix}-glass:before { content: $fa-var-glass; } -.#{$fa-css-prefix}-music:before { content: $fa-var-music; } -.#{$fa-css-prefix}-search:before { content: $fa-var-search; } -.#{$fa-css-prefix}-envelope-o:before { content: $fa-var-envelope-o; } -.#{$fa-css-prefix}-heart:before { content: $fa-var-heart; } -.#{$fa-css-prefix}-star:before { content: $fa-var-star; } -.#{$fa-css-prefix}-star-o:before { content: $fa-var-star-o; } -.#{$fa-css-prefix}-user:before { content: $fa-var-user; } -.#{$fa-css-prefix}-film:before { content: $fa-var-film; } -.#{$fa-css-prefix}-th-large:before { content: $fa-var-th-large; } -.#{$fa-css-prefix}-th:before { content: $fa-var-th; } -.#{$fa-css-prefix}-th-list:before { content: $fa-var-th-list; } -.#{$fa-css-prefix}-check:before { content: $fa-var-check; } -.#{$fa-css-prefix}-remove:before, -.#{$fa-css-prefix}-close:before, -.#{$fa-css-prefix}-times:before { content: $fa-var-times; } -.#{$fa-css-prefix}-search-plus:before { content: $fa-var-search-plus; } -.#{$fa-css-prefix}-search-minus:before { content: $fa-var-search-minus; } -.#{$fa-css-prefix}-power-off:before { content: $fa-var-power-off; } -.#{$fa-css-prefix}-signal:before { content: $fa-var-signal; } -.#{$fa-css-prefix}-gear:before, -.#{$fa-css-prefix}-cog:before { content: $fa-var-cog; } -.#{$fa-css-prefix}-trash-o:before { content: $fa-var-trash-o; } -.#{$fa-css-prefix}-home:before { content: $fa-var-home; } -.#{$fa-css-prefix}-file-o:before { content: $fa-var-file-o; } -.#{$fa-css-prefix}-clock-o:before { content: $fa-var-clock-o; } -.#{$fa-css-prefix}-road:before { content: $fa-var-road; } -.#{$fa-css-prefix}-download:before { content: $fa-var-download; } -.#{$fa-css-prefix}-arrow-circle-o-down:before { content: $fa-var-arrow-circle-o-down; } -.#{$fa-css-prefix}-arrow-circle-o-up:before { content: $fa-var-arrow-circle-o-up; } -.#{$fa-css-prefix}-inbox:before { content: $fa-var-inbox; } -.#{$fa-css-prefix}-play-circle-o:before { content: $fa-var-play-circle-o; } -.#{$fa-css-prefix}-rotate-right:before, -.#{$fa-css-prefix}-repeat:before { content: $fa-var-repeat; } -.#{$fa-css-prefix}-refresh:before { content: $fa-var-refresh; } -.#{$fa-css-prefix}-list-alt:before { content: $fa-var-list-alt; } -.#{$fa-css-prefix}-lock:before { content: $fa-var-lock; } -.#{$fa-css-prefix}-flag:before { content: $fa-var-flag; } -.#{$fa-css-prefix}-headphones:before { content: $fa-var-headphones; } -.#{$fa-css-prefix}-volume-off:before { content: $fa-var-volume-off; } -.#{$fa-css-prefix}-volume-down:before { content: $fa-var-volume-down; } -.#{$fa-css-prefix}-volume-up:before { content: $fa-var-volume-up; } -.#{$fa-css-prefix}-qrcode:before { content: $fa-var-qrcode; } -.#{$fa-css-prefix}-barcode:before { content: $fa-var-barcode; } -.#{$fa-css-prefix}-tag:before { content: $fa-var-tag; } -.#{$fa-css-prefix}-tags:before { content: $fa-var-tags; } -.#{$fa-css-prefix}-book:before { content: $fa-var-book; } -.#{$fa-css-prefix}-bookmark:before { content: $fa-var-bookmark; } -.#{$fa-css-prefix}-print:before { content: $fa-var-print; } -.#{$fa-css-prefix}-camera:before { content: $fa-var-camera; } -.#{$fa-css-prefix}-font:before { content: $fa-var-font; } -.#{$fa-css-prefix}-bold:before { content: $fa-var-bold; } -.#{$fa-css-prefix}-italic:before { content: $fa-var-italic; } -.#{$fa-css-prefix}-text-height:before { content: $fa-var-text-height; } -.#{$fa-css-prefix}-text-width:before { content: $fa-var-text-width; } -.#{$fa-css-prefix}-align-left:before { content: $fa-var-align-left; } -.#{$fa-css-prefix}-align-center:before { content: $fa-var-align-center; } -.#{$fa-css-prefix}-align-right:before { content: $fa-var-align-right; } -.#{$fa-css-prefix}-align-justify:before { content: $fa-var-align-justify; } -.#{$fa-css-prefix}-list:before { content: $fa-var-list; } -.#{$fa-css-prefix}-dedent:before, -.#{$fa-css-prefix}-outdent:before { content: $fa-var-outdent; } -.#{$fa-css-prefix}-indent:before { content: $fa-var-indent; } -.#{$fa-css-prefix}-video-camera:before { content: $fa-var-video-camera; } -.#{$fa-css-prefix}-photo:before, -.#{$fa-css-prefix}-image:before, -.#{$fa-css-prefix}-picture-o:before { content: $fa-var-picture-o; } -.#{$fa-css-prefix}-pencil:before { content: $fa-var-pencil; } -.#{$fa-css-prefix}-map-marker:before { content: $fa-var-map-marker; } -.#{$fa-css-prefix}-adjust:before { content: $fa-var-adjust; } -.#{$fa-css-prefix}-tint:before { content: $fa-var-tint; } -.#{$fa-css-prefix}-edit:before, -.#{$fa-css-prefix}-pencil-square-o:before { content: $fa-var-pencil-square-o; } -.#{$fa-css-prefix}-share-square-o:before { content: $fa-var-share-square-o; } -.#{$fa-css-prefix}-check-square-o:before { content: $fa-var-check-square-o; } -.#{$fa-css-prefix}-arrows:before { content: $fa-var-arrows; } -.#{$fa-css-prefix}-step-backward:before { content: $fa-var-step-backward; } -.#{$fa-css-prefix}-fast-backward:before { content: $fa-var-fast-backward; } -.#{$fa-css-prefix}-backward:before { content: $fa-var-backward; } -.#{$fa-css-prefix}-play:before { content: $fa-var-play; } -.#{$fa-css-prefix}-pause:before { content: $fa-var-pause; } -.#{$fa-css-prefix}-stop:before { content: $fa-var-stop; } -.#{$fa-css-prefix}-forward:before { content: $fa-var-forward; } -.#{$fa-css-prefix}-fast-forward:before { content: $fa-var-fast-forward; } -.#{$fa-css-prefix}-step-forward:before { content: $fa-var-step-forward; } -.#{$fa-css-prefix}-eject:before { content: $fa-var-eject; } -.#{$fa-css-prefix}-chevron-left:before { content: $fa-var-chevron-left; } -.#{$fa-css-prefix}-chevron-right:before { content: $fa-var-chevron-right; } -.#{$fa-css-prefix}-plus-circle:before { content: $fa-var-plus-circle; } -.#{$fa-css-prefix}-minus-circle:before { content: $fa-var-minus-circle; } -.#{$fa-css-prefix}-times-circle:before { content: $fa-var-times-circle; } -.#{$fa-css-prefix}-check-circle:before { content: $fa-var-check-circle; } -.#{$fa-css-prefix}-question-circle:before { content: $fa-var-question-circle; } -.#{$fa-css-prefix}-info-circle:before { content: $fa-var-info-circle; } -.#{$fa-css-prefix}-crosshairs:before { content: $fa-var-crosshairs; } -.#{$fa-css-prefix}-times-circle-o:before { content: $fa-var-times-circle-o; } -.#{$fa-css-prefix}-check-circle-o:before { content: $fa-var-check-circle-o; } -.#{$fa-css-prefix}-ban:before { content: $fa-var-ban; } -.#{$fa-css-prefix}-arrow-left:before { content: $fa-var-arrow-left; } -.#{$fa-css-prefix}-arrow-right:before { content: $fa-var-arrow-right; } -.#{$fa-css-prefix}-arrow-up:before { content: $fa-var-arrow-up; } -.#{$fa-css-prefix}-arrow-down:before { content: $fa-var-arrow-down; } -.#{$fa-css-prefix}-mail-forward:before, -.#{$fa-css-prefix}-share:before { content: $fa-var-share; } -.#{$fa-css-prefix}-expand:before { content: $fa-var-expand; } -.#{$fa-css-prefix}-compress:before { content: $fa-var-compress; } -.#{$fa-css-prefix}-plus:before { content: $fa-var-plus; } -.#{$fa-css-prefix}-minus:before { content: $fa-var-minus; } -.#{$fa-css-prefix}-asterisk:before { content: $fa-var-asterisk; } -.#{$fa-css-prefix}-exclamation-circle:before { content: $fa-var-exclamation-circle; } -.#{$fa-css-prefix}-gift:before { content: $fa-var-gift; } -.#{$fa-css-prefix}-leaf:before { content: $fa-var-leaf; } -.#{$fa-css-prefix}-fire:before { content: $fa-var-fire; } -.#{$fa-css-prefix}-eye:before { content: $fa-var-eye; } -.#{$fa-css-prefix}-eye-slash:before { content: $fa-var-eye-slash; } -.#{$fa-css-prefix}-warning:before, -.#{$fa-css-prefix}-exclamation-triangle:before { content: $fa-var-exclamation-triangle; } -.#{$fa-css-prefix}-plane:before { content: $fa-var-plane; } -.#{$fa-css-prefix}-calendar:before { content: $fa-var-calendar; } -.#{$fa-css-prefix}-random:before { content: $fa-var-random; } -.#{$fa-css-prefix}-comment:before { content: $fa-var-comment; } -.#{$fa-css-prefix}-magnet:before { content: $fa-var-magnet; } -.#{$fa-css-prefix}-chevron-up:before { content: $fa-var-chevron-up; } -.#{$fa-css-prefix}-chevron-down:before { content: $fa-var-chevron-down; } -.#{$fa-css-prefix}-retweet:before { content: $fa-var-retweet; } -.#{$fa-css-prefix}-shopping-cart:before { content: $fa-var-shopping-cart; } -.#{$fa-css-prefix}-folder:before { content: $fa-var-folder; } -.#{$fa-css-prefix}-folder-open:before { content: $fa-var-folder-open; } -.#{$fa-css-prefix}-arrows-v:before { content: $fa-var-arrows-v; } -.#{$fa-css-prefix}-arrows-h:before { content: $fa-var-arrows-h; } -.#{$fa-css-prefix}-bar-chart-o:before, -.#{$fa-css-prefix}-bar-chart:before { content: $fa-var-bar-chart; } -.#{$fa-css-prefix}-twitter-square:before { content: $fa-var-twitter-square; } -.#{$fa-css-prefix}-facebook-square:before { content: $fa-var-facebook-square; } -.#{$fa-css-prefix}-camera-retro:before { content: $fa-var-camera-retro; } -.#{$fa-css-prefix}-key:before { content: $fa-var-key; } -.#{$fa-css-prefix}-gears:before, -.#{$fa-css-prefix}-cogs:before { content: $fa-var-cogs; } -.#{$fa-css-prefix}-comments:before { content: $fa-var-comments; } -.#{$fa-css-prefix}-thumbs-o-up:before { content: $fa-var-thumbs-o-up; } -.#{$fa-css-prefix}-thumbs-o-down:before { content: $fa-var-thumbs-o-down; } -.#{$fa-css-prefix}-star-half:before { content: $fa-var-star-half; } -.#{$fa-css-prefix}-heart-o:before { content: $fa-var-heart-o; } -.#{$fa-css-prefix}-sign-out:before { content: $fa-var-sign-out; } -.#{$fa-css-prefix}-linkedin-square:before { content: $fa-var-linkedin-square; } -.#{$fa-css-prefix}-thumb-tack:before { content: $fa-var-thumb-tack; } -.#{$fa-css-prefix}-external-link:before { content: $fa-var-external-link; } -.#{$fa-css-prefix}-sign-in:before { content: $fa-var-sign-in; } -.#{$fa-css-prefix}-trophy:before { content: $fa-var-trophy; } -.#{$fa-css-prefix}-github-square:before { content: $fa-var-github-square; } -.#{$fa-css-prefix}-upload:before { content: $fa-var-upload; } -.#{$fa-css-prefix}-lemon-o:before { content: $fa-var-lemon-o; } -.#{$fa-css-prefix}-phone:before { content: $fa-var-phone; } -.#{$fa-css-prefix}-square-o:before { content: $fa-var-square-o; } -.#{$fa-css-prefix}-bookmark-o:before { content: $fa-var-bookmark-o; } -.#{$fa-css-prefix}-phone-square:before { content: $fa-var-phone-square; } -.#{$fa-css-prefix}-twitter:before { content: $fa-var-twitter; } -.#{$fa-css-prefix}-facebook-f:before, -.#{$fa-css-prefix}-facebook:before { content: $fa-var-facebook; } -.#{$fa-css-prefix}-github:before { content: $fa-var-github; } -.#{$fa-css-prefix}-unlock:before { content: $fa-var-unlock; } -.#{$fa-css-prefix}-credit-card:before { content: $fa-var-credit-card; } -.#{$fa-css-prefix}-feed:before, -.#{$fa-css-prefix}-rss:before { content: $fa-var-rss; } -.#{$fa-css-prefix}-hdd-o:before { content: $fa-var-hdd-o; } -.#{$fa-css-prefix}-bullhorn:before { content: $fa-var-bullhorn; } -.#{$fa-css-prefix}-bell:before { content: $fa-var-bell; } -.#{$fa-css-prefix}-certificate:before { content: $fa-var-certificate; } -.#{$fa-css-prefix}-hand-o-right:before { content: $fa-var-hand-o-right; } -.#{$fa-css-prefix}-hand-o-left:before { content: $fa-var-hand-o-left; } -.#{$fa-css-prefix}-hand-o-up:before { content: $fa-var-hand-o-up; } -.#{$fa-css-prefix}-hand-o-down:before { content: $fa-var-hand-o-down; } -.#{$fa-css-prefix}-arrow-circle-left:before { content: $fa-var-arrow-circle-left; } -.#{$fa-css-prefix}-arrow-circle-right:before { content: $fa-var-arrow-circle-right; } -.#{$fa-css-prefix}-arrow-circle-up:before { content: $fa-var-arrow-circle-up; } -.#{$fa-css-prefix}-arrow-circle-down:before { content: $fa-var-arrow-circle-down; } -.#{$fa-css-prefix}-globe:before { content: $fa-var-globe; } -.#{$fa-css-prefix}-wrench:before { content: $fa-var-wrench; } -.#{$fa-css-prefix}-tasks:before { content: $fa-var-tasks; } -.#{$fa-css-prefix}-filter:before { content: $fa-var-filter; } -.#{$fa-css-prefix}-briefcase:before { content: $fa-var-briefcase; } -.#{$fa-css-prefix}-arrows-alt:before { content: $fa-var-arrows-alt; } -.#{$fa-css-prefix}-group:before, -.#{$fa-css-prefix}-users:before { content: $fa-var-users; } -.#{$fa-css-prefix}-chain:before, -.#{$fa-css-prefix}-link:before { content: $fa-var-link; } -.#{$fa-css-prefix}-cloud:before { content: $fa-var-cloud; } -.#{$fa-css-prefix}-flask:before { content: $fa-var-flask; } -.#{$fa-css-prefix}-cut:before, -.#{$fa-css-prefix}-scissors:before { content: $fa-var-scissors; } -.#{$fa-css-prefix}-copy:before, -.#{$fa-css-prefix}-files-o:before { content: $fa-var-files-o; } -.#{$fa-css-prefix}-paperclip:before { content: $fa-var-paperclip; } -.#{$fa-css-prefix}-save:before, -.#{$fa-css-prefix}-floppy-o:before { content: $fa-var-floppy-o; } -.#{$fa-css-prefix}-square:before { content: $fa-var-square; } -.#{$fa-css-prefix}-navicon:before, -.#{$fa-css-prefix}-reorder:before, -.#{$fa-css-prefix}-bars:before { content: $fa-var-bars; } -.#{$fa-css-prefix}-list-ul:before { content: $fa-var-list-ul; } -.#{$fa-css-prefix}-list-ol:before { content: $fa-var-list-ol; } -.#{$fa-css-prefix}-strikethrough:before { content: $fa-var-strikethrough; } -.#{$fa-css-prefix}-underline:before { content: $fa-var-underline; } -.#{$fa-css-prefix}-table:before { content: $fa-var-table; } -.#{$fa-css-prefix}-magic:before { content: $fa-var-magic; } -.#{$fa-css-prefix}-truck:before { content: $fa-var-truck; } -.#{$fa-css-prefix}-pinterest:before { content: $fa-var-pinterest; } -.#{$fa-css-prefix}-pinterest-square:before { content: $fa-var-pinterest-square; } -.#{$fa-css-prefix}-google-plus-square:before { content: $fa-var-google-plus-square; } -.#{$fa-css-prefix}-google-plus:before { content: $fa-var-google-plus; } -.#{$fa-css-prefix}-money:before { content: $fa-var-money; } -.#{$fa-css-prefix}-caret-down:before { content: $fa-var-caret-down; } -.#{$fa-css-prefix}-caret-up:before { content: $fa-var-caret-up; } -.#{$fa-css-prefix}-caret-left:before { content: $fa-var-caret-left; } -.#{$fa-css-prefix}-caret-right:before { content: $fa-var-caret-right; } -.#{$fa-css-prefix}-columns:before { content: $fa-var-columns; } -.#{$fa-css-prefix}-unsorted:before, -.#{$fa-css-prefix}-sort:before { content: $fa-var-sort; } -.#{$fa-css-prefix}-sort-down:before, -.#{$fa-css-prefix}-sort-desc:before { content: $fa-var-sort-desc; } -.#{$fa-css-prefix}-sort-up:before, -.#{$fa-css-prefix}-sort-asc:before { content: $fa-var-sort-asc; } -.#{$fa-css-prefix}-envelope:before { content: $fa-var-envelope; } -.#{$fa-css-prefix}-linkedin:before { content: $fa-var-linkedin; } -.#{$fa-css-prefix}-rotate-left:before, -.#{$fa-css-prefix}-undo:before { content: $fa-var-undo; } -.#{$fa-css-prefix}-legal:before, -.#{$fa-css-prefix}-gavel:before { content: $fa-var-gavel; } -.#{$fa-css-prefix}-dashboard:before, -.#{$fa-css-prefix}-tachometer:before { content: $fa-var-tachometer; } -.#{$fa-css-prefix}-comment-o:before { content: $fa-var-comment-o; } -.#{$fa-css-prefix}-comments-o:before { content: $fa-var-comments-o; } -.#{$fa-css-prefix}-flash:before, -.#{$fa-css-prefix}-bolt:before { content: $fa-var-bolt; } -.#{$fa-css-prefix}-sitemap:before { content: $fa-var-sitemap; } -.#{$fa-css-prefix}-umbrella:before { content: $fa-var-umbrella; } -.#{$fa-css-prefix}-paste:before, -.#{$fa-css-prefix}-clipboard:before { content: $fa-var-clipboard; } -.#{$fa-css-prefix}-lightbulb-o:before { content: $fa-var-lightbulb-o; } -.#{$fa-css-prefix}-exchange:before { content: $fa-var-exchange; } -.#{$fa-css-prefix}-cloud-download:before { content: $fa-var-cloud-download; } -.#{$fa-css-prefix}-cloud-upload:before { content: $fa-var-cloud-upload; } -.#{$fa-css-prefix}-user-md:before { content: $fa-var-user-md; } -.#{$fa-css-prefix}-stethoscope:before { content: $fa-var-stethoscope; } -.#{$fa-css-prefix}-suitcase:before { content: $fa-var-suitcase; } -.#{$fa-css-prefix}-bell-o:before { content: $fa-var-bell-o; } -.#{$fa-css-prefix}-coffee:before { content: $fa-var-coffee; } -.#{$fa-css-prefix}-cutlery:before { content: $fa-var-cutlery; } -.#{$fa-css-prefix}-file-text-o:before { content: $fa-var-file-text-o; } -.#{$fa-css-prefix}-building-o:before { content: $fa-var-building-o; } -.#{$fa-css-prefix}-hospital-o:before { content: $fa-var-hospital-o; } -.#{$fa-css-prefix}-ambulance:before { content: $fa-var-ambulance; } -.#{$fa-css-prefix}-medkit:before { content: $fa-var-medkit; } -.#{$fa-css-prefix}-fighter-jet:before { content: $fa-var-fighter-jet; } -.#{$fa-css-prefix}-beer:before { content: $fa-var-beer; } -.#{$fa-css-prefix}-h-square:before { content: $fa-var-h-square; } -.#{$fa-css-prefix}-plus-square:before { content: $fa-var-plus-square; } -.#{$fa-css-prefix}-angle-double-left:before { content: $fa-var-angle-double-left; } -.#{$fa-css-prefix}-angle-double-right:before { content: $fa-var-angle-double-right; } -.#{$fa-css-prefix}-angle-double-up:before { content: $fa-var-angle-double-up; } -.#{$fa-css-prefix}-angle-double-down:before { content: $fa-var-angle-double-down; } -.#{$fa-css-prefix}-angle-left:before { content: $fa-var-angle-left; } -.#{$fa-css-prefix}-angle-right:before { content: $fa-var-angle-right; } -.#{$fa-css-prefix}-angle-up:before { content: $fa-var-angle-up; } -.#{$fa-css-prefix}-angle-down:before { content: $fa-var-angle-down; } -.#{$fa-css-prefix}-desktop:before { content: $fa-var-desktop; } -.#{$fa-css-prefix}-laptop:before { content: $fa-var-laptop; } -.#{$fa-css-prefix}-tablet:before { content: $fa-var-tablet; } -.#{$fa-css-prefix}-mobile-phone:before, -.#{$fa-css-prefix}-mobile:before { content: $fa-var-mobile; } -.#{$fa-css-prefix}-circle-o:before { content: $fa-var-circle-o; } -.#{$fa-css-prefix}-quote-left:before { content: $fa-var-quote-left; } -.#{$fa-css-prefix}-quote-right:before { content: $fa-var-quote-right; } -.#{$fa-css-prefix}-spinner:before { content: $fa-var-spinner; } -.#{$fa-css-prefix}-circle:before { content: $fa-var-circle; } -.#{$fa-css-prefix}-mail-reply:before, -.#{$fa-css-prefix}-reply:before { content: $fa-var-reply; } -.#{$fa-css-prefix}-github-alt:before { content: $fa-var-github-alt; } -.#{$fa-css-prefix}-folder-o:before { content: $fa-var-folder-o; } -.#{$fa-css-prefix}-folder-open-o:before { content: $fa-var-folder-open-o; } -.#{$fa-css-prefix}-smile-o:before { content: $fa-var-smile-o; } -.#{$fa-css-prefix}-frown-o:before { content: $fa-var-frown-o; } -.#{$fa-css-prefix}-meh-o:before { content: $fa-var-meh-o; } -.#{$fa-css-prefix}-gamepad:before { content: $fa-var-gamepad; } -.#{$fa-css-prefix}-keyboard-o:before { content: $fa-var-keyboard-o; } -.#{$fa-css-prefix}-flag-o:before { content: $fa-var-flag-o; } -.#{$fa-css-prefix}-flag-checkered:before { content: $fa-var-flag-checkered; } -.#{$fa-css-prefix}-terminal:before { content: $fa-var-terminal; } -.#{$fa-css-prefix}-code:before { content: $fa-var-code; } -.#{$fa-css-prefix}-mail-reply-all:before, -.#{$fa-css-prefix}-reply-all:before { content: $fa-var-reply-all; } -.#{$fa-css-prefix}-star-half-empty:before, -.#{$fa-css-prefix}-star-half-full:before, -.#{$fa-css-prefix}-star-half-o:before { content: $fa-var-star-half-o; } -.#{$fa-css-prefix}-location-arrow:before { content: $fa-var-location-arrow; } -.#{$fa-css-prefix}-crop:before { content: $fa-var-crop; } -.#{$fa-css-prefix}-code-fork:before { content: $fa-var-code-fork; } -.#{$fa-css-prefix}-unlink:before, -.#{$fa-css-prefix}-chain-broken:before { content: $fa-var-chain-broken; } -.#{$fa-css-prefix}-question:before { content: $fa-var-question; } -.#{$fa-css-prefix}-info:before { content: $fa-var-info; } -.#{$fa-css-prefix}-exclamation:before { content: $fa-var-exclamation; } -.#{$fa-css-prefix}-superscript:before { content: $fa-var-superscript; } -.#{$fa-css-prefix}-subscript:before { content: $fa-var-subscript; } -.#{$fa-css-prefix}-eraser:before { content: $fa-var-eraser; } -.#{$fa-css-prefix}-puzzle-piece:before { content: $fa-var-puzzle-piece; } -.#{$fa-css-prefix}-microphone:before { content: $fa-var-microphone; } -.#{$fa-css-prefix}-microphone-slash:before { content: $fa-var-microphone-slash; } -.#{$fa-css-prefix}-shield:before { content: $fa-var-shield; } -.#{$fa-css-prefix}-calendar-o:before { content: $fa-var-calendar-o; } -.#{$fa-css-prefix}-fire-extinguisher:before { content: $fa-var-fire-extinguisher; } -.#{$fa-css-prefix}-rocket:before { content: $fa-var-rocket; } -.#{$fa-css-prefix}-maxcdn:before { content: $fa-var-maxcdn; } -.#{$fa-css-prefix}-chevron-circle-left:before { content: $fa-var-chevron-circle-left; } -.#{$fa-css-prefix}-chevron-circle-right:before { content: $fa-var-chevron-circle-right; } -.#{$fa-css-prefix}-chevron-circle-up:before { content: $fa-var-chevron-circle-up; } -.#{$fa-css-prefix}-chevron-circle-down:before { content: $fa-var-chevron-circle-down; } -.#{$fa-css-prefix}-html5:before { content: $fa-var-html5; } -.#{$fa-css-prefix}-css3:before { content: $fa-var-css3; } -.#{$fa-css-prefix}-anchor:before { content: $fa-var-anchor; } -.#{$fa-css-prefix}-unlock-alt:before { content: $fa-var-unlock-alt; } -.#{$fa-css-prefix}-bullseye:before { content: $fa-var-bullseye; } -.#{$fa-css-prefix}-ellipsis-h:before { content: $fa-var-ellipsis-h; } -.#{$fa-css-prefix}-ellipsis-v:before { content: $fa-var-ellipsis-v; } -.#{$fa-css-prefix}-rss-square:before { content: $fa-var-rss-square; } -.#{$fa-css-prefix}-play-circle:before { content: $fa-var-play-circle; } -.#{$fa-css-prefix}-ticket:before { content: $fa-var-ticket; } -.#{$fa-css-prefix}-minus-square:before { content: $fa-var-minus-square; } -.#{$fa-css-prefix}-minus-square-o:before { content: $fa-var-minus-square-o; } -.#{$fa-css-prefix}-level-up:before { content: $fa-var-level-up; } -.#{$fa-css-prefix}-level-down:before { content: $fa-var-level-down; } -.#{$fa-css-prefix}-check-square:before { content: $fa-var-check-square; } -.#{$fa-css-prefix}-pencil-square:before { content: $fa-var-pencil-square; } -.#{$fa-css-prefix}-external-link-square:before { content: $fa-var-external-link-square; } -.#{$fa-css-prefix}-share-square:before { content: $fa-var-share-square; } -.#{$fa-css-prefix}-compass:before { content: $fa-var-compass; } -.#{$fa-css-prefix}-toggle-down:before, -.#{$fa-css-prefix}-caret-square-o-down:before { content: $fa-var-caret-square-o-down; } -.#{$fa-css-prefix}-toggle-up:before, -.#{$fa-css-prefix}-caret-square-o-up:before { content: $fa-var-caret-square-o-up; } -.#{$fa-css-prefix}-toggle-right:before, -.#{$fa-css-prefix}-caret-square-o-right:before { content: $fa-var-caret-square-o-right; } -.#{$fa-css-prefix}-euro:before, -.#{$fa-css-prefix}-eur:before { content: $fa-var-eur; } -.#{$fa-css-prefix}-gbp:before { content: $fa-var-gbp; } -.#{$fa-css-prefix}-dollar:before, -.#{$fa-css-prefix}-usd:before { content: $fa-var-usd; } -.#{$fa-css-prefix}-rupee:before, -.#{$fa-css-prefix}-inr:before { content: $fa-var-inr; } -.#{$fa-css-prefix}-cny:before, -.#{$fa-css-prefix}-rmb:before, -.#{$fa-css-prefix}-yen:before, -.#{$fa-css-prefix}-jpy:before { content: $fa-var-jpy; } -.#{$fa-css-prefix}-ruble:before, -.#{$fa-css-prefix}-rouble:before, -.#{$fa-css-prefix}-rub:before { content: $fa-var-rub; } -.#{$fa-css-prefix}-won:before, -.#{$fa-css-prefix}-krw:before { content: $fa-var-krw; } -.#{$fa-css-prefix}-bitcoin:before, -.#{$fa-css-prefix}-btc:before { content: $fa-var-btc; } -.#{$fa-css-prefix}-file:before { content: $fa-var-file; } -.#{$fa-css-prefix}-file-text:before { content: $fa-var-file-text; } -.#{$fa-css-prefix}-sort-alpha-asc:before { content: $fa-var-sort-alpha-asc; } -.#{$fa-css-prefix}-sort-alpha-desc:before { content: $fa-var-sort-alpha-desc; } -.#{$fa-css-prefix}-sort-amount-asc:before { content: $fa-var-sort-amount-asc; } -.#{$fa-css-prefix}-sort-amount-desc:before { content: $fa-var-sort-amount-desc; } -.#{$fa-css-prefix}-sort-numeric-asc:before { content: $fa-var-sort-numeric-asc; } -.#{$fa-css-prefix}-sort-numeric-desc:before { content: $fa-var-sort-numeric-desc; } -.#{$fa-css-prefix}-thumbs-up:before { content: $fa-var-thumbs-up; } -.#{$fa-css-prefix}-thumbs-down:before { content: $fa-var-thumbs-down; } -.#{$fa-css-prefix}-youtube-square:before { content: $fa-var-youtube-square; } -.#{$fa-css-prefix}-youtube:before { content: $fa-var-youtube; } -.#{$fa-css-prefix}-xing:before { content: $fa-var-xing; } -.#{$fa-css-prefix}-xing-square:before { content: $fa-var-xing-square; } -.#{$fa-css-prefix}-youtube-play:before { content: $fa-var-youtube-play; } -.#{$fa-css-prefix}-dropbox:before { content: $fa-var-dropbox; } -.#{$fa-css-prefix}-stack-overflow:before { content: $fa-var-stack-overflow; } -.#{$fa-css-prefix}-instagram:before { content: $fa-var-instagram; } -.#{$fa-css-prefix}-flickr:before { content: $fa-var-flickr; } -.#{$fa-css-prefix}-adn:before { content: $fa-var-adn; } -.#{$fa-css-prefix}-bitbucket:before { content: $fa-var-bitbucket; } -.#{$fa-css-prefix}-bitbucket-square:before { content: $fa-var-bitbucket-square; } -.#{$fa-css-prefix}-tumblr:before { content: $fa-var-tumblr; } -.#{$fa-css-prefix}-tumblr-square:before { content: $fa-var-tumblr-square; } -.#{$fa-css-prefix}-long-arrow-down:before { content: $fa-var-long-arrow-down; } -.#{$fa-css-prefix}-long-arrow-up:before { content: $fa-var-long-arrow-up; } -.#{$fa-css-prefix}-long-arrow-left:before { content: $fa-var-long-arrow-left; } -.#{$fa-css-prefix}-long-arrow-right:before { content: $fa-var-long-arrow-right; } -.#{$fa-css-prefix}-apple:before { content: $fa-var-apple; } -.#{$fa-css-prefix}-windows:before { content: $fa-var-windows; } -.#{$fa-css-prefix}-android:before { content: $fa-var-android; } -.#{$fa-css-prefix}-linux:before { content: $fa-var-linux; } -.#{$fa-css-prefix}-dribbble:before { content: $fa-var-dribbble; } -.#{$fa-css-prefix}-skype:before { content: $fa-var-skype; } -.#{$fa-css-prefix}-foursquare:before { content: $fa-var-foursquare; } -.#{$fa-css-prefix}-trello:before { content: $fa-var-trello; } -.#{$fa-css-prefix}-female:before { content: $fa-var-female; } -.#{$fa-css-prefix}-male:before { content: $fa-var-male; } -.#{$fa-css-prefix}-gittip:before, -.#{$fa-css-prefix}-gratipay:before { content: $fa-var-gratipay; } -.#{$fa-css-prefix}-sun-o:before { content: $fa-var-sun-o; } -.#{$fa-css-prefix}-moon-o:before { content: $fa-var-moon-o; } -.#{$fa-css-prefix}-archive:before { content: $fa-var-archive; } -.#{$fa-css-prefix}-bug:before { content: $fa-var-bug; } -.#{$fa-css-prefix}-vk:before { content: $fa-var-vk; } -.#{$fa-css-prefix}-weibo:before { content: $fa-var-weibo; } -.#{$fa-css-prefix}-renren:before { content: $fa-var-renren; } -.#{$fa-css-prefix}-pagelines:before { content: $fa-var-pagelines; } -.#{$fa-css-prefix}-stack-exchange:before { content: $fa-var-stack-exchange; } -.#{$fa-css-prefix}-arrow-circle-o-right:before { content: $fa-var-arrow-circle-o-right; } -.#{$fa-css-prefix}-arrow-circle-o-left:before { content: $fa-var-arrow-circle-o-left; } -.#{$fa-css-prefix}-toggle-left:before, -.#{$fa-css-prefix}-caret-square-o-left:before { content: $fa-var-caret-square-o-left; } -.#{$fa-css-prefix}-dot-circle-o:before { content: $fa-var-dot-circle-o; } -.#{$fa-css-prefix}-wheelchair:before { content: $fa-var-wheelchair; } -.#{$fa-css-prefix}-vimeo-square:before { content: $fa-var-vimeo-square; } -.#{$fa-css-prefix}-turkish-lira:before, -.#{$fa-css-prefix}-try:before { content: $fa-var-try; } -.#{$fa-css-prefix}-plus-square-o:before { content: $fa-var-plus-square-o; } -.#{$fa-css-prefix}-space-shuttle:before { content: $fa-var-space-shuttle; } -.#{$fa-css-prefix}-slack:before { content: $fa-var-slack; } -.#{$fa-css-prefix}-envelope-square:before { content: $fa-var-envelope-square; } -.#{$fa-css-prefix}-wordpress:before { content: $fa-var-wordpress; } -.#{$fa-css-prefix}-openid:before { content: $fa-var-openid; } -.#{$fa-css-prefix}-institution:before, -.#{$fa-css-prefix}-bank:before, -.#{$fa-css-prefix}-university:before { content: $fa-var-university; } -.#{$fa-css-prefix}-mortar-board:before, -.#{$fa-css-prefix}-graduation-cap:before { content: $fa-var-graduation-cap; } -.#{$fa-css-prefix}-yahoo:before { content: $fa-var-yahoo; } -.#{$fa-css-prefix}-google:before { content: $fa-var-google; } -.#{$fa-css-prefix}-reddit:before { content: $fa-var-reddit; } -.#{$fa-css-prefix}-reddit-square:before { content: $fa-var-reddit-square; } -.#{$fa-css-prefix}-stumbleupon-circle:before { content: $fa-var-stumbleupon-circle; } -.#{$fa-css-prefix}-stumbleupon:before { content: $fa-var-stumbleupon; } -.#{$fa-css-prefix}-delicious:before { content: $fa-var-delicious; } -.#{$fa-css-prefix}-digg:before { content: $fa-var-digg; } -.#{$fa-css-prefix}-pied-piper-pp:before { content: $fa-var-pied-piper-pp; } -.#{$fa-css-prefix}-pied-piper-alt:before { content: $fa-var-pied-piper-alt; } -.#{$fa-css-prefix}-drupal:before { content: $fa-var-drupal; } -.#{$fa-css-prefix}-joomla:before { content: $fa-var-joomla; } -.#{$fa-css-prefix}-language:before { content: $fa-var-language; } -.#{$fa-css-prefix}-fax:before { content: $fa-var-fax; } -.#{$fa-css-prefix}-building:before { content: $fa-var-building; } -.#{$fa-css-prefix}-child:before { content: $fa-var-child; } -.#{$fa-css-prefix}-paw:before { content: $fa-var-paw; } -.#{$fa-css-prefix}-spoon:before { content: $fa-var-spoon; } -.#{$fa-css-prefix}-cube:before { content: $fa-var-cube; } -.#{$fa-css-prefix}-cubes:before { content: $fa-var-cubes; } -.#{$fa-css-prefix}-behance:before { content: $fa-var-behance; } -.#{$fa-css-prefix}-behance-square:before { content: $fa-var-behance-square; } -.#{$fa-css-prefix}-steam:before { content: $fa-var-steam; } -.#{$fa-css-prefix}-steam-square:before { content: $fa-var-steam-square; } -.#{$fa-css-prefix}-recycle:before { content: $fa-var-recycle; } -.#{$fa-css-prefix}-automobile:before, -.#{$fa-css-prefix}-car:before { content: $fa-var-car; } -.#{$fa-css-prefix}-cab:before, -.#{$fa-css-prefix}-taxi:before { content: $fa-var-taxi; } -.#{$fa-css-prefix}-tree:before { content: $fa-var-tree; } -.#{$fa-css-prefix}-spotify:before { content: $fa-var-spotify; } -.#{$fa-css-prefix}-deviantart:before { content: $fa-var-deviantart; } -.#{$fa-css-prefix}-soundcloud:before { content: $fa-var-soundcloud; } -.#{$fa-css-prefix}-database:before { content: $fa-var-database; } -.#{$fa-css-prefix}-file-pdf-o:before { content: $fa-var-file-pdf-o; } -.#{$fa-css-prefix}-file-word-o:before { content: $fa-var-file-word-o; } -.#{$fa-css-prefix}-file-excel-o:before { content: $fa-var-file-excel-o; } -.#{$fa-css-prefix}-file-powerpoint-o:before { content: $fa-var-file-powerpoint-o; } -.#{$fa-css-prefix}-file-photo-o:before, -.#{$fa-css-prefix}-file-picture-o:before, -.#{$fa-css-prefix}-file-image-o:before { content: $fa-var-file-image-o; } -.#{$fa-css-prefix}-file-zip-o:before, -.#{$fa-css-prefix}-file-archive-o:before { content: $fa-var-file-archive-o; } -.#{$fa-css-prefix}-file-sound-o:before, -.#{$fa-css-prefix}-file-audio-o:before { content: $fa-var-file-audio-o; } -.#{$fa-css-prefix}-file-movie-o:before, -.#{$fa-css-prefix}-file-video-o:before { content: $fa-var-file-video-o; } -.#{$fa-css-prefix}-file-code-o:before { content: $fa-var-file-code-o; } -.#{$fa-css-prefix}-vine:before { content: $fa-var-vine; } -.#{$fa-css-prefix}-codepen:before { content: $fa-var-codepen; } -.#{$fa-css-prefix}-jsfiddle:before { content: $fa-var-jsfiddle; } -.#{$fa-css-prefix}-life-bouy:before, -.#{$fa-css-prefix}-life-buoy:before, -.#{$fa-css-prefix}-life-saver:before, -.#{$fa-css-prefix}-support:before, -.#{$fa-css-prefix}-life-ring:before { content: $fa-var-life-ring; } -.#{$fa-css-prefix}-circle-o-notch:before { content: $fa-var-circle-o-notch; } -.#{$fa-css-prefix}-ra:before, -.#{$fa-css-prefix}-resistance:before, -.#{$fa-css-prefix}-rebel:before { content: $fa-var-rebel; } -.#{$fa-css-prefix}-ge:before, -.#{$fa-css-prefix}-empire:before { content: $fa-var-empire; } -.#{$fa-css-prefix}-git-square:before { content: $fa-var-git-square; } -.#{$fa-css-prefix}-git:before { content: $fa-var-git; } -.#{$fa-css-prefix}-y-combinator-square:before, -.#{$fa-css-prefix}-yc-square:before, -.#{$fa-css-prefix}-hacker-news:before { content: $fa-var-hacker-news; } -.#{$fa-css-prefix}-tencent-weibo:before { content: $fa-var-tencent-weibo; } -.#{$fa-css-prefix}-qq:before { content: $fa-var-qq; } -.#{$fa-css-prefix}-wechat:before, -.#{$fa-css-prefix}-weixin:before { content: $fa-var-weixin; } -.#{$fa-css-prefix}-send:before, -.#{$fa-css-prefix}-paper-plane:before { content: $fa-var-paper-plane; } -.#{$fa-css-prefix}-send-o:before, -.#{$fa-css-prefix}-paper-plane-o:before { content: $fa-var-paper-plane-o; } -.#{$fa-css-prefix}-history:before { content: $fa-var-history; } -.#{$fa-css-prefix}-circle-thin:before { content: $fa-var-circle-thin; } -.#{$fa-css-prefix}-header:before { content: $fa-var-header; } -.#{$fa-css-prefix}-paragraph:before { content: $fa-var-paragraph; } -.#{$fa-css-prefix}-sliders:before { content: $fa-var-sliders; } -.#{$fa-css-prefix}-share-alt:before { content: $fa-var-share-alt; } -.#{$fa-css-prefix}-share-alt-square:before { content: $fa-var-share-alt-square; } -.#{$fa-css-prefix}-bomb:before { content: $fa-var-bomb; } -.#{$fa-css-prefix}-soccer-ball-o:before, -.#{$fa-css-prefix}-futbol-o:before { content: $fa-var-futbol-o; } -.#{$fa-css-prefix}-tty:before { content: $fa-var-tty; } -.#{$fa-css-prefix}-binoculars:before { content: $fa-var-binoculars; } -.#{$fa-css-prefix}-plug:before { content: $fa-var-plug; } -.#{$fa-css-prefix}-slideshare:before { content: $fa-var-slideshare; } -.#{$fa-css-prefix}-twitch:before { content: $fa-var-twitch; } -.#{$fa-css-prefix}-yelp:before { content: $fa-var-yelp; } -.#{$fa-css-prefix}-newspaper-o:before { content: $fa-var-newspaper-o; } -.#{$fa-css-prefix}-wifi:before { content: $fa-var-wifi; } -.#{$fa-css-prefix}-calculator:before { content: $fa-var-calculator; } -.#{$fa-css-prefix}-paypal:before { content: $fa-var-paypal; } -.#{$fa-css-prefix}-google-wallet:before { content: $fa-var-google-wallet; } -.#{$fa-css-prefix}-cc-visa:before { content: $fa-var-cc-visa; } -.#{$fa-css-prefix}-cc-mastercard:before { content: $fa-var-cc-mastercard; } -.#{$fa-css-prefix}-cc-discover:before { content: $fa-var-cc-discover; } -.#{$fa-css-prefix}-cc-amex:before { content: $fa-var-cc-amex; } -.#{$fa-css-prefix}-cc-paypal:before { content: $fa-var-cc-paypal; } -.#{$fa-css-prefix}-cc-stripe:before { content: $fa-var-cc-stripe; } -.#{$fa-css-prefix}-bell-slash:before { content: $fa-var-bell-slash; } -.#{$fa-css-prefix}-bell-slash-o:before { content: $fa-var-bell-slash-o; } -.#{$fa-css-prefix}-trash:before { content: $fa-var-trash; } -.#{$fa-css-prefix}-copyright:before { content: $fa-var-copyright; } -.#{$fa-css-prefix}-at:before { content: $fa-var-at; } -.#{$fa-css-prefix}-eyedropper:before { content: $fa-var-eyedropper; } -.#{$fa-css-prefix}-paint-brush:before { content: $fa-var-paint-brush; } -.#{$fa-css-prefix}-birthday-cake:before { content: $fa-var-birthday-cake; } -.#{$fa-css-prefix}-area-chart:before { content: $fa-var-area-chart; } -.#{$fa-css-prefix}-pie-chart:before { content: $fa-var-pie-chart; } -.#{$fa-css-prefix}-line-chart:before { content: $fa-var-line-chart; } -.#{$fa-css-prefix}-lastfm:before { content: $fa-var-lastfm; } -.#{$fa-css-prefix}-lastfm-square:before { content: $fa-var-lastfm-square; } -.#{$fa-css-prefix}-toggle-off:before { content: $fa-var-toggle-off; } -.#{$fa-css-prefix}-toggle-on:before { content: $fa-var-toggle-on; } -.#{$fa-css-prefix}-bicycle:before { content: $fa-var-bicycle; } -.#{$fa-css-prefix}-bus:before { content: $fa-var-bus; } -.#{$fa-css-prefix}-ioxhost:before { content: $fa-var-ioxhost; } -.#{$fa-css-prefix}-angellist:before { content: $fa-var-angellist; } -.#{$fa-css-prefix}-cc:before { content: $fa-var-cc; } -.#{$fa-css-prefix}-shekel:before, -.#{$fa-css-prefix}-sheqel:before, -.#{$fa-css-prefix}-ils:before { content: $fa-var-ils; } -.#{$fa-css-prefix}-meanpath:before { content: $fa-var-meanpath; } -.#{$fa-css-prefix}-buysellads:before { content: $fa-var-buysellads; } -.#{$fa-css-prefix}-connectdevelop:before { content: $fa-var-connectdevelop; } -.#{$fa-css-prefix}-dashcube:before { content: $fa-var-dashcube; } -.#{$fa-css-prefix}-forumbee:before { content: $fa-var-forumbee; } -.#{$fa-css-prefix}-leanpub:before { content: $fa-var-leanpub; } -.#{$fa-css-prefix}-sellsy:before { content: $fa-var-sellsy; } -.#{$fa-css-prefix}-shirtsinbulk:before { content: $fa-var-shirtsinbulk; } -.#{$fa-css-prefix}-simplybuilt:before { content: $fa-var-simplybuilt; } -.#{$fa-css-prefix}-skyatlas:before { content: $fa-var-skyatlas; } -.#{$fa-css-prefix}-cart-plus:before { content: $fa-var-cart-plus; } -.#{$fa-css-prefix}-cart-arrow-down:before { content: $fa-var-cart-arrow-down; } -.#{$fa-css-prefix}-diamond:before { content: $fa-var-diamond; } -.#{$fa-css-prefix}-ship:before { content: $fa-var-ship; } -.#{$fa-css-prefix}-user-secret:before { content: $fa-var-user-secret; } -.#{$fa-css-prefix}-motorcycle:before { content: $fa-var-motorcycle; } -.#{$fa-css-prefix}-street-view:before { content: $fa-var-street-view; } -.#{$fa-css-prefix}-heartbeat:before { content: $fa-var-heartbeat; } -.#{$fa-css-prefix}-venus:before { content: $fa-var-venus; } -.#{$fa-css-prefix}-mars:before { content: $fa-var-mars; } -.#{$fa-css-prefix}-mercury:before { content: $fa-var-mercury; } -.#{$fa-css-prefix}-intersex:before, -.#{$fa-css-prefix}-transgender:before { content: $fa-var-transgender; } -.#{$fa-css-prefix}-transgender-alt:before { content: $fa-var-transgender-alt; } -.#{$fa-css-prefix}-venus-double:before { content: $fa-var-venus-double; } -.#{$fa-css-prefix}-mars-double:before { content: $fa-var-mars-double; } -.#{$fa-css-prefix}-venus-mars:before { content: $fa-var-venus-mars; } -.#{$fa-css-prefix}-mars-stroke:before { content: $fa-var-mars-stroke; } -.#{$fa-css-prefix}-mars-stroke-v:before { content: $fa-var-mars-stroke-v; } -.#{$fa-css-prefix}-mars-stroke-h:before { content: $fa-var-mars-stroke-h; } -.#{$fa-css-prefix}-neuter:before { content: $fa-var-neuter; } -.#{$fa-css-prefix}-genderless:before { content: $fa-var-genderless; } -.#{$fa-css-prefix}-facebook-official:before { content: $fa-var-facebook-official; } -.#{$fa-css-prefix}-pinterest-p:before { content: $fa-var-pinterest-p; } -.#{$fa-css-prefix}-whatsapp:before { content: $fa-var-whatsapp; } -.#{$fa-css-prefix}-server:before { content: $fa-var-server; } -.#{$fa-css-prefix}-user-plus:before { content: $fa-var-user-plus; } -.#{$fa-css-prefix}-user-times:before { content: $fa-var-user-times; } -.#{$fa-css-prefix}-hotel:before, -.#{$fa-css-prefix}-bed:before { content: $fa-var-bed; } -.#{$fa-css-prefix}-viacoin:before { content: $fa-var-viacoin; } -.#{$fa-css-prefix}-train:before { content: $fa-var-train; } -.#{$fa-css-prefix}-subway:before { content: $fa-var-subway; } -.#{$fa-css-prefix}-medium:before { content: $fa-var-medium; } -.#{$fa-css-prefix}-yc:before, -.#{$fa-css-prefix}-y-combinator:before { content: $fa-var-y-combinator; } -.#{$fa-css-prefix}-optin-monster:before { content: $fa-var-optin-monster; } -.#{$fa-css-prefix}-opencart:before { content: $fa-var-opencart; } -.#{$fa-css-prefix}-expeditedssl:before { content: $fa-var-expeditedssl; } -.#{$fa-css-prefix}-battery-4:before, -.#{$fa-css-prefix}-battery:before, -.#{$fa-css-prefix}-battery-full:before { content: $fa-var-battery-full; } -.#{$fa-css-prefix}-battery-3:before, -.#{$fa-css-prefix}-battery-three-quarters:before { content: $fa-var-battery-three-quarters; } -.#{$fa-css-prefix}-battery-2:before, -.#{$fa-css-prefix}-battery-half:before { content: $fa-var-battery-half; } -.#{$fa-css-prefix}-battery-1:before, -.#{$fa-css-prefix}-battery-quarter:before { content: $fa-var-battery-quarter; } -.#{$fa-css-prefix}-battery-0:before, -.#{$fa-css-prefix}-battery-empty:before { content: $fa-var-battery-empty; } -.#{$fa-css-prefix}-mouse-pointer:before { content: $fa-var-mouse-pointer; } -.#{$fa-css-prefix}-i-cursor:before { content: $fa-var-i-cursor; } -.#{$fa-css-prefix}-object-group:before { content: $fa-var-object-group; } -.#{$fa-css-prefix}-object-ungroup:before { content: $fa-var-object-ungroup; } -.#{$fa-css-prefix}-sticky-note:before { content: $fa-var-sticky-note; } -.#{$fa-css-prefix}-sticky-note-o:before { content: $fa-var-sticky-note-o; } -.#{$fa-css-prefix}-cc-jcb:before { content: $fa-var-cc-jcb; } -.#{$fa-css-prefix}-cc-diners-club:before { content: $fa-var-cc-diners-club; } -.#{$fa-css-prefix}-clone:before { content: $fa-var-clone; } -.#{$fa-css-prefix}-balance-scale:before { content: $fa-var-balance-scale; } -.#{$fa-css-prefix}-hourglass-o:before { content: $fa-var-hourglass-o; } -.#{$fa-css-prefix}-hourglass-1:before, -.#{$fa-css-prefix}-hourglass-start:before { content: $fa-var-hourglass-start; } -.#{$fa-css-prefix}-hourglass-2:before, -.#{$fa-css-prefix}-hourglass-half:before { content: $fa-var-hourglass-half; } -.#{$fa-css-prefix}-hourglass-3:before, -.#{$fa-css-prefix}-hourglass-end:before { content: $fa-var-hourglass-end; } -.#{$fa-css-prefix}-hourglass:before { content: $fa-var-hourglass; } -.#{$fa-css-prefix}-hand-grab-o:before, -.#{$fa-css-prefix}-hand-rock-o:before { content: $fa-var-hand-rock-o; } -.#{$fa-css-prefix}-hand-stop-o:before, -.#{$fa-css-prefix}-hand-paper-o:before { content: $fa-var-hand-paper-o; } -.#{$fa-css-prefix}-hand-scissors-o:before { content: $fa-var-hand-scissors-o; } -.#{$fa-css-prefix}-hand-lizard-o:before { content: $fa-var-hand-lizard-o; } -.#{$fa-css-prefix}-hand-spock-o:before { content: $fa-var-hand-spock-o; } -.#{$fa-css-prefix}-hand-pointer-o:before { content: $fa-var-hand-pointer-o; } -.#{$fa-css-prefix}-hand-peace-o:before { content: $fa-var-hand-peace-o; } -.#{$fa-css-prefix}-trademark:before { content: $fa-var-trademark; } -.#{$fa-css-prefix}-registered:before { content: $fa-var-registered; } -.#{$fa-css-prefix}-creative-commons:before { content: $fa-var-creative-commons; } -.#{$fa-css-prefix}-gg:before { content: $fa-var-gg; } -.#{$fa-css-prefix}-gg-circle:before { content: $fa-var-gg-circle; } -.#{$fa-css-prefix}-tripadvisor:before { content: $fa-var-tripadvisor; } -.#{$fa-css-prefix}-odnoklassniki:before { content: $fa-var-odnoklassniki; } -.#{$fa-css-prefix}-odnoklassniki-square:before { content: $fa-var-odnoklassniki-square; } -.#{$fa-css-prefix}-get-pocket:before { content: $fa-var-get-pocket; } -.#{$fa-css-prefix}-wikipedia-w:before { content: $fa-var-wikipedia-w; } -.#{$fa-css-prefix}-safari:before { content: $fa-var-safari; } -.#{$fa-css-prefix}-chrome:before { content: $fa-var-chrome; } -.#{$fa-css-prefix}-firefox:before { content: $fa-var-firefox; } -.#{$fa-css-prefix}-opera:before { content: $fa-var-opera; } -.#{$fa-css-prefix}-internet-explorer:before { content: $fa-var-internet-explorer; } -.#{$fa-css-prefix}-tv:before, -.#{$fa-css-prefix}-television:before { content: $fa-var-television; } -.#{$fa-css-prefix}-contao:before { content: $fa-var-contao; } -.#{$fa-css-prefix}-500px:before { content: $fa-var-500px; } -.#{$fa-css-prefix}-amazon:before { content: $fa-var-amazon; } -.#{$fa-css-prefix}-calendar-plus-o:before { content: $fa-var-calendar-plus-o; } -.#{$fa-css-prefix}-calendar-minus-o:before { content: $fa-var-calendar-minus-o; } -.#{$fa-css-prefix}-calendar-times-o:before { content: $fa-var-calendar-times-o; } -.#{$fa-css-prefix}-calendar-check-o:before { content: $fa-var-calendar-check-o; } -.#{$fa-css-prefix}-industry:before { content: $fa-var-industry; } -.#{$fa-css-prefix}-map-pin:before { content: $fa-var-map-pin; } -.#{$fa-css-prefix}-map-signs:before { content: $fa-var-map-signs; } -.#{$fa-css-prefix}-map-o:before { content: $fa-var-map-o; } -.#{$fa-css-prefix}-map:before { content: $fa-var-map; } -.#{$fa-css-prefix}-commenting:before { content: $fa-var-commenting; } -.#{$fa-css-prefix}-commenting-o:before { content: $fa-var-commenting-o; } -.#{$fa-css-prefix}-houzz:before { content: $fa-var-houzz; } -.#{$fa-css-prefix}-vimeo:before { content: $fa-var-vimeo; } -.#{$fa-css-prefix}-black-tie:before { content: $fa-var-black-tie; } -.#{$fa-css-prefix}-fonticons:before { content: $fa-var-fonticons; } -.#{$fa-css-prefix}-reddit-alien:before { content: $fa-var-reddit-alien; } -.#{$fa-css-prefix}-edge:before { content: $fa-var-edge; } -.#{$fa-css-prefix}-credit-card-alt:before { content: $fa-var-credit-card-alt; } -.#{$fa-css-prefix}-codiepie:before { content: $fa-var-codiepie; } -.#{$fa-css-prefix}-modx:before { content: $fa-var-modx; } -.#{$fa-css-prefix}-fort-awesome:before { content: $fa-var-fort-awesome; } -.#{$fa-css-prefix}-usb:before { content: $fa-var-usb; } -.#{$fa-css-prefix}-product-hunt:before { content: $fa-var-product-hunt; } -.#{$fa-css-prefix}-mixcloud:before { content: $fa-var-mixcloud; } -.#{$fa-css-prefix}-scribd:before { content: $fa-var-scribd; } -.#{$fa-css-prefix}-pause-circle:before { content: $fa-var-pause-circle; } -.#{$fa-css-prefix}-pause-circle-o:before { content: $fa-var-pause-circle-o; } -.#{$fa-css-prefix}-stop-circle:before { content: $fa-var-stop-circle; } -.#{$fa-css-prefix}-stop-circle-o:before { content: $fa-var-stop-circle-o; } -.#{$fa-css-prefix}-shopping-bag:before { content: $fa-var-shopping-bag; } -.#{$fa-css-prefix}-shopping-basket:before { content: $fa-var-shopping-basket; } -.#{$fa-css-prefix}-hashtag:before { content: $fa-var-hashtag; } -.#{$fa-css-prefix}-bluetooth:before { content: $fa-var-bluetooth; } -.#{$fa-css-prefix}-bluetooth-b:before { content: $fa-var-bluetooth-b; } -.#{$fa-css-prefix}-percent:before { content: $fa-var-percent; } -.#{$fa-css-prefix}-gitlab:before { content: $fa-var-gitlab; } -.#{$fa-css-prefix}-wpbeginner:before { content: $fa-var-wpbeginner; } -.#{$fa-css-prefix}-wpforms:before { content: $fa-var-wpforms; } -.#{$fa-css-prefix}-envira:before { content: $fa-var-envira; } -.#{$fa-css-prefix}-universal-access:before { content: $fa-var-universal-access; } -.#{$fa-css-prefix}-wheelchair-alt:before { content: $fa-var-wheelchair-alt; } -.#{$fa-css-prefix}-question-circle-o:before { content: $fa-var-question-circle-o; } -.#{$fa-css-prefix}-blind:before { content: $fa-var-blind; } -.#{$fa-css-prefix}-audio-description:before { content: $fa-var-audio-description; } -.#{$fa-css-prefix}-volume-control-phone:before { content: $fa-var-volume-control-phone; } -.#{$fa-css-prefix}-braille:before { content: $fa-var-braille; } -.#{$fa-css-prefix}-assistive-listening-systems:before { content: $fa-var-assistive-listening-systems; } -.#{$fa-css-prefix}-asl-interpreting:before, -.#{$fa-css-prefix}-american-sign-language-interpreting:before { content: $fa-var-american-sign-language-interpreting; } -.#{$fa-css-prefix}-deafness:before, -.#{$fa-css-prefix}-hard-of-hearing:before, -.#{$fa-css-prefix}-deaf:before { content: $fa-var-deaf; } -.#{$fa-css-prefix}-glide:before { content: $fa-var-glide; } -.#{$fa-css-prefix}-glide-g:before { content: $fa-var-glide-g; } -.#{$fa-css-prefix}-signing:before, -.#{$fa-css-prefix}-sign-language:before { content: $fa-var-sign-language; } -.#{$fa-css-prefix}-low-vision:before { content: $fa-var-low-vision; } -.#{$fa-css-prefix}-viadeo:before { content: $fa-var-viadeo; } -.#{$fa-css-prefix}-viadeo-square:before { content: $fa-var-viadeo-square; } -.#{$fa-css-prefix}-snapchat:before { content: $fa-var-snapchat; } -.#{$fa-css-prefix}-snapchat-ghost:before { content: $fa-var-snapchat-ghost; } -.#{$fa-css-prefix}-snapchat-square:before { content: $fa-var-snapchat-square; } -.#{$fa-css-prefix}-pied-piper:before { content: $fa-var-pied-piper; } -.#{$fa-css-prefix}-first-order:before { content: $fa-var-first-order; } -.#{$fa-css-prefix}-yoast:before { content: $fa-var-yoast; } -.#{$fa-css-prefix}-themeisle:before { content: $fa-var-themeisle; } -.#{$fa-css-prefix}-google-plus-circle:before, -.#{$fa-css-prefix}-google-plus-official:before { content: $fa-var-google-plus-official; } -.#{$fa-css-prefix}-fa:before, -.#{$fa-css-prefix}-font-awesome:before { content: $fa-var-font-awesome; } -.#{$fa-css-prefix}-handshake-o:before { content: $fa-var-handshake-o; } -.#{$fa-css-prefix}-envelope-open:before { content: $fa-var-envelope-open; } -.#{$fa-css-prefix}-envelope-open-o:before { content: $fa-var-envelope-open-o; } -.#{$fa-css-prefix}-linode:before { content: $fa-var-linode; } -.#{$fa-css-prefix}-address-book:before { content: $fa-var-address-book; } -.#{$fa-css-prefix}-address-book-o:before { content: $fa-var-address-book-o; } -.#{$fa-css-prefix}-vcard:before, -.#{$fa-css-prefix}-address-card:before { content: $fa-var-address-card; } -.#{$fa-css-prefix}-vcard-o:before, -.#{$fa-css-prefix}-address-card-o:before { content: $fa-var-address-card-o; } -.#{$fa-css-prefix}-user-circle:before { content: $fa-var-user-circle; } -.#{$fa-css-prefix}-user-circle-o:before { content: $fa-var-user-circle-o; } -.#{$fa-css-prefix}-user-o:before { content: $fa-var-user-o; } -.#{$fa-css-prefix}-id-badge:before { content: $fa-var-id-badge; } -.#{$fa-css-prefix}-drivers-license:before, -.#{$fa-css-prefix}-id-card:before { content: $fa-var-id-card; } -.#{$fa-css-prefix}-drivers-license-o:before, -.#{$fa-css-prefix}-id-card-o:before { content: $fa-var-id-card-o; } -.#{$fa-css-prefix}-quora:before { content: $fa-var-quora; } -.#{$fa-css-prefix}-free-code-camp:before { content: $fa-var-free-code-camp; } -.#{$fa-css-prefix}-telegram:before { content: $fa-var-telegram; } -.#{$fa-css-prefix}-thermometer-4:before, -.#{$fa-css-prefix}-thermometer:before, -.#{$fa-css-prefix}-thermometer-full:before { content: $fa-var-thermometer-full; } -.#{$fa-css-prefix}-thermometer-3:before, -.#{$fa-css-prefix}-thermometer-three-quarters:before { content: $fa-var-thermometer-three-quarters; } -.#{$fa-css-prefix}-thermometer-2:before, -.#{$fa-css-prefix}-thermometer-half:before { content: $fa-var-thermometer-half; } -.#{$fa-css-prefix}-thermometer-1:before, -.#{$fa-css-prefix}-thermometer-quarter:before { content: $fa-var-thermometer-quarter; } -.#{$fa-css-prefix}-thermometer-0:before, -.#{$fa-css-prefix}-thermometer-empty:before { content: $fa-var-thermometer-empty; } -.#{$fa-css-prefix}-shower:before { content: $fa-var-shower; } -.#{$fa-css-prefix}-bathtub:before, -.#{$fa-css-prefix}-s15:before, -.#{$fa-css-prefix}-bath:before { content: $fa-var-bath; } -.#{$fa-css-prefix}-podcast:before { content: $fa-var-podcast; } -.#{$fa-css-prefix}-window-maximize:before { content: $fa-var-window-maximize; } -.#{$fa-css-prefix}-window-minimize:before { content: $fa-var-window-minimize; } -.#{$fa-css-prefix}-window-restore:before { content: $fa-var-window-restore; } -.#{$fa-css-prefix}-times-rectangle:before, -.#{$fa-css-prefix}-window-close:before { content: $fa-var-window-close; } -.#{$fa-css-prefix}-times-rectangle-o:before, -.#{$fa-css-prefix}-window-close-o:before { content: $fa-var-window-close-o; } -.#{$fa-css-prefix}-bandcamp:before { content: $fa-var-bandcamp; } -.#{$fa-css-prefix}-grav:before { content: $fa-var-grav; } -.#{$fa-css-prefix}-etsy:before { content: $fa-var-etsy; } -.#{$fa-css-prefix}-imdb:before { content: $fa-var-imdb; } -.#{$fa-css-prefix}-ravelry:before { content: $fa-var-ravelry; } -.#{$fa-css-prefix}-eercast:before { content: $fa-var-eercast; } -.#{$fa-css-prefix}-microchip:before { content: $fa-var-microchip; } -.#{$fa-css-prefix}-snowflake-o:before { content: $fa-var-snowflake-o; } -.#{$fa-css-prefix}-superpowers:before { content: $fa-var-superpowers; } -.#{$fa-css-prefix}-wpexplorer:before { content: $fa-var-wpexplorer; } -.#{$fa-css-prefix}-meetup:before { content: $fa-var-meetup; } diff --git a/resources/public/font-awesome-4.7.0/scss/_larger.scss b/resources/public/font-awesome-4.7.0/scss/_larger.scss deleted file mode 100644 index 41e9a8184..000000000 --- a/resources/public/font-awesome-4.7.0/scss/_larger.scss +++ /dev/null @@ -1,13 +0,0 @@ -// Icon Sizes -// ------------------------- - -/* makes the font 33% larger relative to the icon container */ -.#{$fa-css-prefix}-lg { - font-size: (4em / 3); - line-height: (3em / 4); - vertical-align: -15%; -} -.#{$fa-css-prefix}-2x { font-size: 2em; } -.#{$fa-css-prefix}-3x { font-size: 3em; } -.#{$fa-css-prefix}-4x { font-size: 4em; } -.#{$fa-css-prefix}-5x { font-size: 5em; } diff --git a/resources/public/font-awesome-4.7.0/scss/_list.scss b/resources/public/font-awesome-4.7.0/scss/_list.scss deleted file mode 100644 index 7d1e4d54d..000000000 --- a/resources/public/font-awesome-4.7.0/scss/_list.scss +++ /dev/null @@ -1,19 +0,0 @@ -// List Icons -// ------------------------- - -.#{$fa-css-prefix}-ul { - padding-left: 0; - margin-left: $fa-li-width; - list-style-type: none; - > li { position: relative; } -} -.#{$fa-css-prefix}-li { - position: absolute; - left: -$fa-li-width; - width: $fa-li-width; - top: (2em / 14); - text-align: center; - &.#{$fa-css-prefix}-lg { - left: -$fa-li-width + (4em / 14); - } -} diff --git a/resources/public/font-awesome-4.7.0/scss/_mixins.scss b/resources/public/font-awesome-4.7.0/scss/_mixins.scss deleted file mode 100644 index c3bbd5745..000000000 --- a/resources/public/font-awesome-4.7.0/scss/_mixins.scss +++ /dev/null @@ -1,60 +0,0 @@ -// Mixins -// -------------------------- - -@mixin fa-icon() { - display: inline-block; - font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration - font-size: inherit; // can't have font-size inherit on line above, so need to override - text-rendering: auto; // optimizelegibility throws things off #1094 - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - -} - -@mixin fa-icon-rotate($degrees, $rotation) { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; - -webkit-transform: rotate($degrees); - -ms-transform: rotate($degrees); - transform: rotate($degrees); -} - -@mixin fa-icon-flip($horiz, $vert, $rotation) { - -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; - -webkit-transform: scale($horiz, $vert); - -ms-transform: scale($horiz, $vert); - transform: scale($horiz, $vert); -} - - -// Only display content to screen readers. A la Bootstrap 4. -// -// See: http://a11yproject.com/posts/how-to-hide-content/ - -@mixin sr-only { - position: absolute; - width: 1px; - height: 1px; - padding: 0; - margin: -1px; - overflow: hidden; - clip: rect(0,0,0,0); - border: 0; -} - -// Use in conjunction with .sr-only to only display content when it's focused. -// -// Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 -// -// Credit: HTML5 Boilerplate - -@mixin sr-only-focusable { - &:active, - &:focus { - position: static; - width: auto; - height: auto; - margin: 0; - overflow: visible; - clip: auto; - } -} diff --git a/resources/public/font-awesome-4.7.0/scss/_path.scss b/resources/public/font-awesome-4.7.0/scss/_path.scss deleted file mode 100644 index bb457c23a..000000000 --- a/resources/public/font-awesome-4.7.0/scss/_path.scss +++ /dev/null @@ -1,15 +0,0 @@ -/* FONT PATH - * -------------------------- */ - -@font-face { - font-family: 'FontAwesome'; - src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); - src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), - url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), - url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), - url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), - url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); -// src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts - font-weight: normal; - font-style: normal; -} diff --git a/resources/public/font-awesome-4.7.0/scss/_rotated-flipped.scss b/resources/public/font-awesome-4.7.0/scss/_rotated-flipped.scss deleted file mode 100644 index a3558fd09..000000000 --- a/resources/public/font-awesome-4.7.0/scss/_rotated-flipped.scss +++ /dev/null @@ -1,20 +0,0 @@ -// Rotated & Flipped Icons -// ------------------------- - -.#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } -.#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } -.#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } - -.#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } -.#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } - -// Hook for IE8-9 -// ------------------------- - -:root .#{$fa-css-prefix}-rotate-90, -:root .#{$fa-css-prefix}-rotate-180, -:root .#{$fa-css-prefix}-rotate-270, -:root .#{$fa-css-prefix}-flip-horizontal, -:root .#{$fa-css-prefix}-flip-vertical { - filter: none; -} diff --git a/resources/public/font-awesome-4.7.0/scss/_screen-reader.scss b/resources/public/font-awesome-4.7.0/scss/_screen-reader.scss deleted file mode 100644 index 637426f0d..000000000 --- a/resources/public/font-awesome-4.7.0/scss/_screen-reader.scss +++ /dev/null @@ -1,5 +0,0 @@ -// Screen Readers -// ------------------------- - -.sr-only { @include sr-only(); } -.sr-only-focusable { @include sr-only-focusable(); } diff --git a/resources/public/font-awesome-4.7.0/scss/_stacked.scss b/resources/public/font-awesome-4.7.0/scss/_stacked.scss deleted file mode 100644 index aef740366..000000000 --- a/resources/public/font-awesome-4.7.0/scss/_stacked.scss +++ /dev/null @@ -1,20 +0,0 @@ -// Stacked Icons -// ------------------------- - -.#{$fa-css-prefix}-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; -} -.#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; -} -.#{$fa-css-prefix}-stack-1x { line-height: inherit; } -.#{$fa-css-prefix}-stack-2x { font-size: 2em; } -.#{$fa-css-prefix}-inverse { color: $fa-inverse; } diff --git a/resources/public/font-awesome-4.7.0/scss/_variables.scss b/resources/public/font-awesome-4.7.0/scss/_variables.scss deleted file mode 100644 index 498fc4a08..000000000 --- a/resources/public/font-awesome-4.7.0/scss/_variables.scss +++ /dev/null @@ -1,800 +0,0 @@ -// Variables -// -------------------------- - -$fa-font-path: "../fonts" !default; -$fa-font-size-base: 14px !default; -$fa-line-height-base: 1 !default; -//$fa-font-path: "//netdna.bootstrapcdn.com/font-awesome/4.7.0/fonts" !default; // for referencing Bootstrap CDN font files directly -$fa-css-prefix: fa !default; -$fa-version: "4.7.0" !default; -$fa-border-color: #eee !default; -$fa-inverse: #fff !default; -$fa-li-width: (30em / 14) !default; - -$fa-var-500px: "\f26e"; -$fa-var-address-book: "\f2b9"; -$fa-var-address-book-o: "\f2ba"; -$fa-var-address-card: "\f2bb"; -$fa-var-address-card-o: "\f2bc"; -$fa-var-adjust: "\f042"; -$fa-var-adn: "\f170"; -$fa-var-align-center: "\f037"; -$fa-var-align-justify: "\f039"; -$fa-var-align-left: "\f036"; -$fa-var-align-right: "\f038"; -$fa-var-amazon: "\f270"; -$fa-var-ambulance: "\f0f9"; -$fa-var-american-sign-language-interpreting: "\f2a3"; -$fa-var-anchor: "\f13d"; -$fa-var-android: "\f17b"; -$fa-var-angellist: "\f209"; -$fa-var-angle-double-down: "\f103"; -$fa-var-angle-double-left: "\f100"; -$fa-var-angle-double-right: "\f101"; -$fa-var-angle-double-up: "\f102"; -$fa-var-angle-down: "\f107"; -$fa-var-angle-left: "\f104"; -$fa-var-angle-right: "\f105"; -$fa-var-angle-up: "\f106"; -$fa-var-apple: "\f179"; -$fa-var-archive: "\f187"; -$fa-var-area-chart: "\f1fe"; -$fa-var-arrow-circle-down: "\f0ab"; -$fa-var-arrow-circle-left: "\f0a8"; -$fa-var-arrow-circle-o-down: "\f01a"; -$fa-var-arrow-circle-o-left: "\f190"; -$fa-var-arrow-circle-o-right: "\f18e"; -$fa-var-arrow-circle-o-up: "\f01b"; -$fa-var-arrow-circle-right: "\f0a9"; -$fa-var-arrow-circle-up: "\f0aa"; -$fa-var-arrow-down: "\f063"; -$fa-var-arrow-left: "\f060"; -$fa-var-arrow-right: "\f061"; -$fa-var-arrow-up: "\f062"; -$fa-var-arrows: "\f047"; -$fa-var-arrows-alt: "\f0b2"; -$fa-var-arrows-h: "\f07e"; -$fa-var-arrows-v: "\f07d"; -$fa-var-asl-interpreting: "\f2a3"; -$fa-var-assistive-listening-systems: "\f2a2"; -$fa-var-asterisk: "\f069"; -$fa-var-at: "\f1fa"; -$fa-var-audio-description: "\f29e"; -$fa-var-automobile: "\f1b9"; -$fa-var-backward: "\f04a"; -$fa-var-balance-scale: "\f24e"; -$fa-var-ban: "\f05e"; -$fa-var-bandcamp: "\f2d5"; -$fa-var-bank: "\f19c"; -$fa-var-bar-chart: "\f080"; -$fa-var-bar-chart-o: "\f080"; -$fa-var-barcode: "\f02a"; -$fa-var-bars: "\f0c9"; -$fa-var-bath: "\f2cd"; -$fa-var-bathtub: "\f2cd"; -$fa-var-battery: "\f240"; -$fa-var-battery-0: "\f244"; -$fa-var-battery-1: "\f243"; -$fa-var-battery-2: "\f242"; -$fa-var-battery-3: "\f241"; -$fa-var-battery-4: "\f240"; -$fa-var-battery-empty: "\f244"; -$fa-var-battery-full: "\f240"; -$fa-var-battery-half: "\f242"; -$fa-var-battery-quarter: "\f243"; -$fa-var-battery-three-quarters: "\f241"; -$fa-var-bed: "\f236"; -$fa-var-beer: "\f0fc"; -$fa-var-behance: "\f1b4"; -$fa-var-behance-square: "\f1b5"; -$fa-var-bell: "\f0f3"; -$fa-var-bell-o: "\f0a2"; -$fa-var-bell-slash: "\f1f6"; -$fa-var-bell-slash-o: "\f1f7"; -$fa-var-bicycle: "\f206"; -$fa-var-binoculars: "\f1e5"; -$fa-var-birthday-cake: "\f1fd"; -$fa-var-bitbucket: "\f171"; -$fa-var-bitbucket-square: "\f172"; -$fa-var-bitcoin: "\f15a"; -$fa-var-black-tie: "\f27e"; -$fa-var-blind: "\f29d"; -$fa-var-bluetooth: "\f293"; -$fa-var-bluetooth-b: "\f294"; -$fa-var-bold: "\f032"; -$fa-var-bolt: "\f0e7"; -$fa-var-bomb: "\f1e2"; -$fa-var-book: "\f02d"; -$fa-var-bookmark: "\f02e"; -$fa-var-bookmark-o: "\f097"; -$fa-var-braille: "\f2a1"; -$fa-var-briefcase: "\f0b1"; -$fa-var-btc: "\f15a"; -$fa-var-bug: "\f188"; -$fa-var-building: "\f1ad"; -$fa-var-building-o: "\f0f7"; -$fa-var-bullhorn: "\f0a1"; -$fa-var-bullseye: "\f140"; -$fa-var-bus: "\f207"; -$fa-var-buysellads: "\f20d"; -$fa-var-cab: "\f1ba"; -$fa-var-calculator: "\f1ec"; -$fa-var-calendar: "\f073"; -$fa-var-calendar-check-o: "\f274"; -$fa-var-calendar-minus-o: "\f272"; -$fa-var-calendar-o: "\f133"; -$fa-var-calendar-plus-o: "\f271"; -$fa-var-calendar-times-o: "\f273"; -$fa-var-camera: "\f030"; -$fa-var-camera-retro: "\f083"; -$fa-var-car: "\f1b9"; -$fa-var-caret-down: "\f0d7"; -$fa-var-caret-left: "\f0d9"; -$fa-var-caret-right: "\f0da"; -$fa-var-caret-square-o-down: "\f150"; -$fa-var-caret-square-o-left: "\f191"; -$fa-var-caret-square-o-right: "\f152"; -$fa-var-caret-square-o-up: "\f151"; -$fa-var-caret-up: "\f0d8"; -$fa-var-cart-arrow-down: "\f218"; -$fa-var-cart-plus: "\f217"; -$fa-var-cc: "\f20a"; -$fa-var-cc-amex: "\f1f3"; -$fa-var-cc-diners-club: "\f24c"; -$fa-var-cc-discover: "\f1f2"; -$fa-var-cc-jcb: "\f24b"; -$fa-var-cc-mastercard: "\f1f1"; -$fa-var-cc-paypal: "\f1f4"; -$fa-var-cc-stripe: "\f1f5"; -$fa-var-cc-visa: "\f1f0"; -$fa-var-certificate: "\f0a3"; -$fa-var-chain: "\f0c1"; -$fa-var-chain-broken: "\f127"; -$fa-var-check: "\f00c"; -$fa-var-check-circle: "\f058"; -$fa-var-check-circle-o: "\f05d"; -$fa-var-check-square: "\f14a"; -$fa-var-check-square-o: "\f046"; -$fa-var-chevron-circle-down: "\f13a"; -$fa-var-chevron-circle-left: "\f137"; -$fa-var-chevron-circle-right: "\f138"; -$fa-var-chevron-circle-up: "\f139"; -$fa-var-chevron-down: "\f078"; -$fa-var-chevron-left: "\f053"; -$fa-var-chevron-right: "\f054"; -$fa-var-chevron-up: "\f077"; -$fa-var-child: "\f1ae"; -$fa-var-chrome: "\f268"; -$fa-var-circle: "\f111"; -$fa-var-circle-o: "\f10c"; -$fa-var-circle-o-notch: "\f1ce"; -$fa-var-circle-thin: "\f1db"; -$fa-var-clipboard: "\f0ea"; -$fa-var-clock-o: "\f017"; -$fa-var-clone: "\f24d"; -$fa-var-close: "\f00d"; -$fa-var-cloud: "\f0c2"; -$fa-var-cloud-download: "\f0ed"; -$fa-var-cloud-upload: "\f0ee"; -$fa-var-cny: "\f157"; -$fa-var-code: "\f121"; -$fa-var-code-fork: "\f126"; -$fa-var-codepen: "\f1cb"; -$fa-var-codiepie: "\f284"; -$fa-var-coffee: "\f0f4"; -$fa-var-cog: "\f013"; -$fa-var-cogs: "\f085"; -$fa-var-columns: "\f0db"; -$fa-var-comment: "\f075"; -$fa-var-comment-o: "\f0e5"; -$fa-var-commenting: "\f27a"; -$fa-var-commenting-o: "\f27b"; -$fa-var-comments: "\f086"; -$fa-var-comments-o: "\f0e6"; -$fa-var-compass: "\f14e"; -$fa-var-compress: "\f066"; -$fa-var-connectdevelop: "\f20e"; -$fa-var-contao: "\f26d"; -$fa-var-copy: "\f0c5"; -$fa-var-copyright: "\f1f9"; -$fa-var-creative-commons: "\f25e"; -$fa-var-credit-card: "\f09d"; -$fa-var-credit-card-alt: "\f283"; -$fa-var-crop: "\f125"; -$fa-var-crosshairs: "\f05b"; -$fa-var-css3: "\f13c"; -$fa-var-cube: "\f1b2"; -$fa-var-cubes: "\f1b3"; -$fa-var-cut: "\f0c4"; -$fa-var-cutlery: "\f0f5"; -$fa-var-dashboard: "\f0e4"; -$fa-var-dashcube: "\f210"; -$fa-var-database: "\f1c0"; -$fa-var-deaf: "\f2a4"; -$fa-var-deafness: "\f2a4"; -$fa-var-dedent: "\f03b"; -$fa-var-delicious: "\f1a5"; -$fa-var-desktop: "\f108"; -$fa-var-deviantart: "\f1bd"; -$fa-var-diamond: "\f219"; -$fa-var-digg: "\f1a6"; -$fa-var-dollar: "\f155"; -$fa-var-dot-circle-o: "\f192"; -$fa-var-download: "\f019"; -$fa-var-dribbble: "\f17d"; -$fa-var-drivers-license: "\f2c2"; -$fa-var-drivers-license-o: "\f2c3"; -$fa-var-dropbox: "\f16b"; -$fa-var-drupal: "\f1a9"; -$fa-var-edge: "\f282"; -$fa-var-edit: "\f044"; -$fa-var-eercast: "\f2da"; -$fa-var-eject: "\f052"; -$fa-var-ellipsis-h: "\f141"; -$fa-var-ellipsis-v: "\f142"; -$fa-var-empire: "\f1d1"; -$fa-var-envelope: "\f0e0"; -$fa-var-envelope-o: "\f003"; -$fa-var-envelope-open: "\f2b6"; -$fa-var-envelope-open-o: "\f2b7"; -$fa-var-envelope-square: "\f199"; -$fa-var-envira: "\f299"; -$fa-var-eraser: "\f12d"; -$fa-var-etsy: "\f2d7"; -$fa-var-eur: "\f153"; -$fa-var-euro: "\f153"; -$fa-var-exchange: "\f0ec"; -$fa-var-exclamation: "\f12a"; -$fa-var-exclamation-circle: "\f06a"; -$fa-var-exclamation-triangle: "\f071"; -$fa-var-expand: "\f065"; -$fa-var-expeditedssl: "\f23e"; -$fa-var-external-link: "\f08e"; -$fa-var-external-link-square: "\f14c"; -$fa-var-eye: "\f06e"; -$fa-var-eye-slash: "\f070"; -$fa-var-eyedropper: "\f1fb"; -$fa-var-fa: "\f2b4"; -$fa-var-facebook: "\f09a"; -$fa-var-facebook-f: "\f09a"; -$fa-var-facebook-official: "\f230"; -$fa-var-facebook-square: "\f082"; -$fa-var-fast-backward: "\f049"; -$fa-var-fast-forward: "\f050"; -$fa-var-fax: "\f1ac"; -$fa-var-feed: "\f09e"; -$fa-var-female: "\f182"; -$fa-var-fighter-jet: "\f0fb"; -$fa-var-file: "\f15b"; -$fa-var-file-archive-o: "\f1c6"; -$fa-var-file-audio-o: "\f1c7"; -$fa-var-file-code-o: "\f1c9"; -$fa-var-file-excel-o: "\f1c3"; -$fa-var-file-image-o: "\f1c5"; -$fa-var-file-movie-o: "\f1c8"; -$fa-var-file-o: "\f016"; -$fa-var-file-pdf-o: "\f1c1"; -$fa-var-file-photo-o: "\f1c5"; -$fa-var-file-picture-o: "\f1c5"; -$fa-var-file-powerpoint-o: "\f1c4"; -$fa-var-file-sound-o: "\f1c7"; -$fa-var-file-text: "\f15c"; -$fa-var-file-text-o: "\f0f6"; -$fa-var-file-video-o: "\f1c8"; -$fa-var-file-word-o: "\f1c2"; -$fa-var-file-zip-o: "\f1c6"; -$fa-var-files-o: "\f0c5"; -$fa-var-film: "\f008"; -$fa-var-filter: "\f0b0"; -$fa-var-fire: "\f06d"; -$fa-var-fire-extinguisher: "\f134"; -$fa-var-firefox: "\f269"; -$fa-var-first-order: "\f2b0"; -$fa-var-flag: "\f024"; -$fa-var-flag-checkered: "\f11e"; -$fa-var-flag-o: "\f11d"; -$fa-var-flash: "\f0e7"; -$fa-var-flask: "\f0c3"; -$fa-var-flickr: "\f16e"; -$fa-var-floppy-o: "\f0c7"; -$fa-var-folder: "\f07b"; -$fa-var-folder-o: "\f114"; -$fa-var-folder-open: "\f07c"; -$fa-var-folder-open-o: "\f115"; -$fa-var-font: "\f031"; -$fa-var-font-awesome: "\f2b4"; -$fa-var-fonticons: "\f280"; -$fa-var-fort-awesome: "\f286"; -$fa-var-forumbee: "\f211"; -$fa-var-forward: "\f04e"; -$fa-var-foursquare: "\f180"; -$fa-var-free-code-camp: "\f2c5"; -$fa-var-frown-o: "\f119"; -$fa-var-futbol-o: "\f1e3"; -$fa-var-gamepad: "\f11b"; -$fa-var-gavel: "\f0e3"; -$fa-var-gbp: "\f154"; -$fa-var-ge: "\f1d1"; -$fa-var-gear: "\f013"; -$fa-var-gears: "\f085"; -$fa-var-genderless: "\f22d"; -$fa-var-get-pocket: "\f265"; -$fa-var-gg: "\f260"; -$fa-var-gg-circle: "\f261"; -$fa-var-gift: "\f06b"; -$fa-var-git: "\f1d3"; -$fa-var-git-square: "\f1d2"; -$fa-var-github: "\f09b"; -$fa-var-github-alt: "\f113"; -$fa-var-github-square: "\f092"; -$fa-var-gitlab: "\f296"; -$fa-var-gittip: "\f184"; -$fa-var-glass: "\f000"; -$fa-var-glide: "\f2a5"; -$fa-var-glide-g: "\f2a6"; -$fa-var-globe: "\f0ac"; -$fa-var-google: "\f1a0"; -$fa-var-google-plus: "\f0d5"; -$fa-var-google-plus-circle: "\f2b3"; -$fa-var-google-plus-official: "\f2b3"; -$fa-var-google-plus-square: "\f0d4"; -$fa-var-google-wallet: "\f1ee"; -$fa-var-graduation-cap: "\f19d"; -$fa-var-gratipay: "\f184"; -$fa-var-grav: "\f2d6"; -$fa-var-group: "\f0c0"; -$fa-var-h-square: "\f0fd"; -$fa-var-hacker-news: "\f1d4"; -$fa-var-hand-grab-o: "\f255"; -$fa-var-hand-lizard-o: "\f258"; -$fa-var-hand-o-down: "\f0a7"; -$fa-var-hand-o-left: "\f0a5"; -$fa-var-hand-o-right: "\f0a4"; -$fa-var-hand-o-up: "\f0a6"; -$fa-var-hand-paper-o: "\f256"; -$fa-var-hand-peace-o: "\f25b"; -$fa-var-hand-pointer-o: "\f25a"; -$fa-var-hand-rock-o: "\f255"; -$fa-var-hand-scissors-o: "\f257"; -$fa-var-hand-spock-o: "\f259"; -$fa-var-hand-stop-o: "\f256"; -$fa-var-handshake-o: "\f2b5"; -$fa-var-hard-of-hearing: "\f2a4"; -$fa-var-hashtag: "\f292"; -$fa-var-hdd-o: "\f0a0"; -$fa-var-header: "\f1dc"; -$fa-var-headphones: "\f025"; -$fa-var-heart: "\f004"; -$fa-var-heart-o: "\f08a"; -$fa-var-heartbeat: "\f21e"; -$fa-var-history: "\f1da"; -$fa-var-home: "\f015"; -$fa-var-hospital-o: "\f0f8"; -$fa-var-hotel: "\f236"; -$fa-var-hourglass: "\f254"; -$fa-var-hourglass-1: "\f251"; -$fa-var-hourglass-2: "\f252"; -$fa-var-hourglass-3: "\f253"; -$fa-var-hourglass-end: "\f253"; -$fa-var-hourglass-half: "\f252"; -$fa-var-hourglass-o: "\f250"; -$fa-var-hourglass-start: "\f251"; -$fa-var-houzz: "\f27c"; -$fa-var-html5: "\f13b"; -$fa-var-i-cursor: "\f246"; -$fa-var-id-badge: "\f2c1"; -$fa-var-id-card: "\f2c2"; -$fa-var-id-card-o: "\f2c3"; -$fa-var-ils: "\f20b"; -$fa-var-image: "\f03e"; -$fa-var-imdb: "\f2d8"; -$fa-var-inbox: "\f01c"; -$fa-var-indent: "\f03c"; -$fa-var-industry: "\f275"; -$fa-var-info: "\f129"; -$fa-var-info-circle: "\f05a"; -$fa-var-inr: "\f156"; -$fa-var-instagram: "\f16d"; -$fa-var-institution: "\f19c"; -$fa-var-internet-explorer: "\f26b"; -$fa-var-intersex: "\f224"; -$fa-var-ioxhost: "\f208"; -$fa-var-italic: "\f033"; -$fa-var-joomla: "\f1aa"; -$fa-var-jpy: "\f157"; -$fa-var-jsfiddle: "\f1cc"; -$fa-var-key: "\f084"; -$fa-var-keyboard-o: "\f11c"; -$fa-var-krw: "\f159"; -$fa-var-language: "\f1ab"; -$fa-var-laptop: "\f109"; -$fa-var-lastfm: "\f202"; -$fa-var-lastfm-square: "\f203"; -$fa-var-leaf: "\f06c"; -$fa-var-leanpub: "\f212"; -$fa-var-legal: "\f0e3"; -$fa-var-lemon-o: "\f094"; -$fa-var-level-down: "\f149"; -$fa-var-level-up: "\f148"; -$fa-var-life-bouy: "\f1cd"; -$fa-var-life-buoy: "\f1cd"; -$fa-var-life-ring: "\f1cd"; -$fa-var-life-saver: "\f1cd"; -$fa-var-lightbulb-o: "\f0eb"; -$fa-var-line-chart: "\f201"; -$fa-var-link: "\f0c1"; -$fa-var-linkedin: "\f0e1"; -$fa-var-linkedin-square: "\f08c"; -$fa-var-linode: "\f2b8"; -$fa-var-linux: "\f17c"; -$fa-var-list: "\f03a"; -$fa-var-list-alt: "\f022"; -$fa-var-list-ol: "\f0cb"; -$fa-var-list-ul: "\f0ca"; -$fa-var-location-arrow: "\f124"; -$fa-var-lock: "\f023"; -$fa-var-long-arrow-down: "\f175"; -$fa-var-long-arrow-left: "\f177"; -$fa-var-long-arrow-right: "\f178"; -$fa-var-long-arrow-up: "\f176"; -$fa-var-low-vision: "\f2a8"; -$fa-var-magic: "\f0d0"; -$fa-var-magnet: "\f076"; -$fa-var-mail-forward: "\f064"; -$fa-var-mail-reply: "\f112"; -$fa-var-mail-reply-all: "\f122"; -$fa-var-male: "\f183"; -$fa-var-map: "\f279"; -$fa-var-map-marker: "\f041"; -$fa-var-map-o: "\f278"; -$fa-var-map-pin: "\f276"; -$fa-var-map-signs: "\f277"; -$fa-var-mars: "\f222"; -$fa-var-mars-double: "\f227"; -$fa-var-mars-stroke: "\f229"; -$fa-var-mars-stroke-h: "\f22b"; -$fa-var-mars-stroke-v: "\f22a"; -$fa-var-maxcdn: "\f136"; -$fa-var-meanpath: "\f20c"; -$fa-var-medium: "\f23a"; -$fa-var-medkit: "\f0fa"; -$fa-var-meetup: "\f2e0"; -$fa-var-meh-o: "\f11a"; -$fa-var-mercury: "\f223"; -$fa-var-microchip: "\f2db"; -$fa-var-microphone: "\f130"; -$fa-var-microphone-slash: "\f131"; -$fa-var-minus: "\f068"; -$fa-var-minus-circle: "\f056"; -$fa-var-minus-square: "\f146"; -$fa-var-minus-square-o: "\f147"; -$fa-var-mixcloud: "\f289"; -$fa-var-mobile: "\f10b"; -$fa-var-mobile-phone: "\f10b"; -$fa-var-modx: "\f285"; -$fa-var-money: "\f0d6"; -$fa-var-moon-o: "\f186"; -$fa-var-mortar-board: "\f19d"; -$fa-var-motorcycle: "\f21c"; -$fa-var-mouse-pointer: "\f245"; -$fa-var-music: "\f001"; -$fa-var-navicon: "\f0c9"; -$fa-var-neuter: "\f22c"; -$fa-var-newspaper-o: "\f1ea"; -$fa-var-object-group: "\f247"; -$fa-var-object-ungroup: "\f248"; -$fa-var-odnoklassniki: "\f263"; -$fa-var-odnoklassniki-square: "\f264"; -$fa-var-opencart: "\f23d"; -$fa-var-openid: "\f19b"; -$fa-var-opera: "\f26a"; -$fa-var-optin-monster: "\f23c"; -$fa-var-outdent: "\f03b"; -$fa-var-pagelines: "\f18c"; -$fa-var-paint-brush: "\f1fc"; -$fa-var-paper-plane: "\f1d8"; -$fa-var-paper-plane-o: "\f1d9"; -$fa-var-paperclip: "\f0c6"; -$fa-var-paragraph: "\f1dd"; -$fa-var-paste: "\f0ea"; -$fa-var-pause: "\f04c"; -$fa-var-pause-circle: "\f28b"; -$fa-var-pause-circle-o: "\f28c"; -$fa-var-paw: "\f1b0"; -$fa-var-paypal: "\f1ed"; -$fa-var-pencil: "\f040"; -$fa-var-pencil-square: "\f14b"; -$fa-var-pencil-square-o: "\f044"; -$fa-var-percent: "\f295"; -$fa-var-phone: "\f095"; -$fa-var-phone-square: "\f098"; -$fa-var-photo: "\f03e"; -$fa-var-picture-o: "\f03e"; -$fa-var-pie-chart: "\f200"; -$fa-var-pied-piper: "\f2ae"; -$fa-var-pied-piper-alt: "\f1a8"; -$fa-var-pied-piper-pp: "\f1a7"; -$fa-var-pinterest: "\f0d2"; -$fa-var-pinterest-p: "\f231"; -$fa-var-pinterest-square: "\f0d3"; -$fa-var-plane: "\f072"; -$fa-var-play: "\f04b"; -$fa-var-play-circle: "\f144"; -$fa-var-play-circle-o: "\f01d"; -$fa-var-plug: "\f1e6"; -$fa-var-plus: "\f067"; -$fa-var-plus-circle: "\f055"; -$fa-var-plus-square: "\f0fe"; -$fa-var-plus-square-o: "\f196"; -$fa-var-podcast: "\f2ce"; -$fa-var-power-off: "\f011"; -$fa-var-print: "\f02f"; -$fa-var-product-hunt: "\f288"; -$fa-var-puzzle-piece: "\f12e"; -$fa-var-qq: "\f1d6"; -$fa-var-qrcode: "\f029"; -$fa-var-question: "\f128"; -$fa-var-question-circle: "\f059"; -$fa-var-question-circle-o: "\f29c"; -$fa-var-quora: "\f2c4"; -$fa-var-quote-left: "\f10d"; -$fa-var-quote-right: "\f10e"; -$fa-var-ra: "\f1d0"; -$fa-var-random: "\f074"; -$fa-var-ravelry: "\f2d9"; -$fa-var-rebel: "\f1d0"; -$fa-var-recycle: "\f1b8"; -$fa-var-reddit: "\f1a1"; -$fa-var-reddit-alien: "\f281"; -$fa-var-reddit-square: "\f1a2"; -$fa-var-refresh: "\f021"; -$fa-var-registered: "\f25d"; -$fa-var-remove: "\f00d"; -$fa-var-renren: "\f18b"; -$fa-var-reorder: "\f0c9"; -$fa-var-repeat: "\f01e"; -$fa-var-reply: "\f112"; -$fa-var-reply-all: "\f122"; -$fa-var-resistance: "\f1d0"; -$fa-var-retweet: "\f079"; -$fa-var-rmb: "\f157"; -$fa-var-road: "\f018"; -$fa-var-rocket: "\f135"; -$fa-var-rotate-left: "\f0e2"; -$fa-var-rotate-right: "\f01e"; -$fa-var-rouble: "\f158"; -$fa-var-rss: "\f09e"; -$fa-var-rss-square: "\f143"; -$fa-var-rub: "\f158"; -$fa-var-ruble: "\f158"; -$fa-var-rupee: "\f156"; -$fa-var-s15: "\f2cd"; -$fa-var-safari: "\f267"; -$fa-var-save: "\f0c7"; -$fa-var-scissors: "\f0c4"; -$fa-var-scribd: "\f28a"; -$fa-var-search: "\f002"; -$fa-var-search-minus: "\f010"; -$fa-var-search-plus: "\f00e"; -$fa-var-sellsy: "\f213"; -$fa-var-send: "\f1d8"; -$fa-var-send-o: "\f1d9"; -$fa-var-server: "\f233"; -$fa-var-share: "\f064"; -$fa-var-share-alt: "\f1e0"; -$fa-var-share-alt-square: "\f1e1"; -$fa-var-share-square: "\f14d"; -$fa-var-share-square-o: "\f045"; -$fa-var-shekel: "\f20b"; -$fa-var-sheqel: "\f20b"; -$fa-var-shield: "\f132"; -$fa-var-ship: "\f21a"; -$fa-var-shirtsinbulk: "\f214"; -$fa-var-shopping-bag: "\f290"; -$fa-var-shopping-basket: "\f291"; -$fa-var-shopping-cart: "\f07a"; -$fa-var-shower: "\f2cc"; -$fa-var-sign-in: "\f090"; -$fa-var-sign-language: "\f2a7"; -$fa-var-sign-out: "\f08b"; -$fa-var-signal: "\f012"; -$fa-var-signing: "\f2a7"; -$fa-var-simplybuilt: "\f215"; -$fa-var-sitemap: "\f0e8"; -$fa-var-skyatlas: "\f216"; -$fa-var-skype: "\f17e"; -$fa-var-slack: "\f198"; -$fa-var-sliders: "\f1de"; -$fa-var-slideshare: "\f1e7"; -$fa-var-smile-o: "\f118"; -$fa-var-snapchat: "\f2ab"; -$fa-var-snapchat-ghost: "\f2ac"; -$fa-var-snapchat-square: "\f2ad"; -$fa-var-snowflake-o: "\f2dc"; -$fa-var-soccer-ball-o: "\f1e3"; -$fa-var-sort: "\f0dc"; -$fa-var-sort-alpha-asc: "\f15d"; -$fa-var-sort-alpha-desc: "\f15e"; -$fa-var-sort-amount-asc: "\f160"; -$fa-var-sort-amount-desc: "\f161"; -$fa-var-sort-asc: "\f0de"; -$fa-var-sort-desc: "\f0dd"; -$fa-var-sort-down: "\f0dd"; -$fa-var-sort-numeric-asc: "\f162"; -$fa-var-sort-numeric-desc: "\f163"; -$fa-var-sort-up: "\f0de"; -$fa-var-soundcloud: "\f1be"; -$fa-var-space-shuttle: "\f197"; -$fa-var-spinner: "\f110"; -$fa-var-spoon: "\f1b1"; -$fa-var-spotify: "\f1bc"; -$fa-var-square: "\f0c8"; -$fa-var-square-o: "\f096"; -$fa-var-stack-exchange: "\f18d"; -$fa-var-stack-overflow: "\f16c"; -$fa-var-star: "\f005"; -$fa-var-star-half: "\f089"; -$fa-var-star-half-empty: "\f123"; -$fa-var-star-half-full: "\f123"; -$fa-var-star-half-o: "\f123"; -$fa-var-star-o: "\f006"; -$fa-var-steam: "\f1b6"; -$fa-var-steam-square: "\f1b7"; -$fa-var-step-backward: "\f048"; -$fa-var-step-forward: "\f051"; -$fa-var-stethoscope: "\f0f1"; -$fa-var-sticky-note: "\f249"; -$fa-var-sticky-note-o: "\f24a"; -$fa-var-stop: "\f04d"; -$fa-var-stop-circle: "\f28d"; -$fa-var-stop-circle-o: "\f28e"; -$fa-var-street-view: "\f21d"; -$fa-var-strikethrough: "\f0cc"; -$fa-var-stumbleupon: "\f1a4"; -$fa-var-stumbleupon-circle: "\f1a3"; -$fa-var-subscript: "\f12c"; -$fa-var-subway: "\f239"; -$fa-var-suitcase: "\f0f2"; -$fa-var-sun-o: "\f185"; -$fa-var-superpowers: "\f2dd"; -$fa-var-superscript: "\f12b"; -$fa-var-support: "\f1cd"; -$fa-var-table: "\f0ce"; -$fa-var-tablet: "\f10a"; -$fa-var-tachometer: "\f0e4"; -$fa-var-tag: "\f02b"; -$fa-var-tags: "\f02c"; -$fa-var-tasks: "\f0ae"; -$fa-var-taxi: "\f1ba"; -$fa-var-telegram: "\f2c6"; -$fa-var-television: "\f26c"; -$fa-var-tencent-weibo: "\f1d5"; -$fa-var-terminal: "\f120"; -$fa-var-text-height: "\f034"; -$fa-var-text-width: "\f035"; -$fa-var-th: "\f00a"; -$fa-var-th-large: "\f009"; -$fa-var-th-list: "\f00b"; -$fa-var-themeisle: "\f2b2"; -$fa-var-thermometer: "\f2c7"; -$fa-var-thermometer-0: "\f2cb"; -$fa-var-thermometer-1: "\f2ca"; -$fa-var-thermometer-2: "\f2c9"; -$fa-var-thermometer-3: "\f2c8"; -$fa-var-thermometer-4: "\f2c7"; -$fa-var-thermometer-empty: "\f2cb"; -$fa-var-thermometer-full: "\f2c7"; -$fa-var-thermometer-half: "\f2c9"; -$fa-var-thermometer-quarter: "\f2ca"; -$fa-var-thermometer-three-quarters: "\f2c8"; -$fa-var-thumb-tack: "\f08d"; -$fa-var-thumbs-down: "\f165"; -$fa-var-thumbs-o-down: "\f088"; -$fa-var-thumbs-o-up: "\f087"; -$fa-var-thumbs-up: "\f164"; -$fa-var-ticket: "\f145"; -$fa-var-times: "\f00d"; -$fa-var-times-circle: "\f057"; -$fa-var-times-circle-o: "\f05c"; -$fa-var-times-rectangle: "\f2d3"; -$fa-var-times-rectangle-o: "\f2d4"; -$fa-var-tint: "\f043"; -$fa-var-toggle-down: "\f150"; -$fa-var-toggle-left: "\f191"; -$fa-var-toggle-off: "\f204"; -$fa-var-toggle-on: "\f205"; -$fa-var-toggle-right: "\f152"; -$fa-var-toggle-up: "\f151"; -$fa-var-trademark: "\f25c"; -$fa-var-train: "\f238"; -$fa-var-transgender: "\f224"; -$fa-var-transgender-alt: "\f225"; -$fa-var-trash: "\f1f8"; -$fa-var-trash-o: "\f014"; -$fa-var-tree: "\f1bb"; -$fa-var-trello: "\f181"; -$fa-var-tripadvisor: "\f262"; -$fa-var-trophy: "\f091"; -$fa-var-truck: "\f0d1"; -$fa-var-try: "\f195"; -$fa-var-tty: "\f1e4"; -$fa-var-tumblr: "\f173"; -$fa-var-tumblr-square: "\f174"; -$fa-var-turkish-lira: "\f195"; -$fa-var-tv: "\f26c"; -$fa-var-twitch: "\f1e8"; -$fa-var-twitter: "\f099"; -$fa-var-twitter-square: "\f081"; -$fa-var-umbrella: "\f0e9"; -$fa-var-underline: "\f0cd"; -$fa-var-undo: "\f0e2"; -$fa-var-universal-access: "\f29a"; -$fa-var-university: "\f19c"; -$fa-var-unlink: "\f127"; -$fa-var-unlock: "\f09c"; -$fa-var-unlock-alt: "\f13e"; -$fa-var-unsorted: "\f0dc"; -$fa-var-upload: "\f093"; -$fa-var-usb: "\f287"; -$fa-var-usd: "\f155"; -$fa-var-user: "\f007"; -$fa-var-user-circle: "\f2bd"; -$fa-var-user-circle-o: "\f2be"; -$fa-var-user-md: "\f0f0"; -$fa-var-user-o: "\f2c0"; -$fa-var-user-plus: "\f234"; -$fa-var-user-secret: "\f21b"; -$fa-var-user-times: "\f235"; -$fa-var-users: "\f0c0"; -$fa-var-vcard: "\f2bb"; -$fa-var-vcard-o: "\f2bc"; -$fa-var-venus: "\f221"; -$fa-var-venus-double: "\f226"; -$fa-var-venus-mars: "\f228"; -$fa-var-viacoin: "\f237"; -$fa-var-viadeo: "\f2a9"; -$fa-var-viadeo-square: "\f2aa"; -$fa-var-video-camera: "\f03d"; -$fa-var-vimeo: "\f27d"; -$fa-var-vimeo-square: "\f194"; -$fa-var-vine: "\f1ca"; -$fa-var-vk: "\f189"; -$fa-var-volume-control-phone: "\f2a0"; -$fa-var-volume-down: "\f027"; -$fa-var-volume-off: "\f026"; -$fa-var-volume-up: "\f028"; -$fa-var-warning: "\f071"; -$fa-var-wechat: "\f1d7"; -$fa-var-weibo: "\f18a"; -$fa-var-weixin: "\f1d7"; -$fa-var-whatsapp: "\f232"; -$fa-var-wheelchair: "\f193"; -$fa-var-wheelchair-alt: "\f29b"; -$fa-var-wifi: "\f1eb"; -$fa-var-wikipedia-w: "\f266"; -$fa-var-window-close: "\f2d3"; -$fa-var-window-close-o: "\f2d4"; -$fa-var-window-maximize: "\f2d0"; -$fa-var-window-minimize: "\f2d1"; -$fa-var-window-restore: "\f2d2"; -$fa-var-windows: "\f17a"; -$fa-var-won: "\f159"; -$fa-var-wordpress: "\f19a"; -$fa-var-wpbeginner: "\f297"; -$fa-var-wpexplorer: "\f2de"; -$fa-var-wpforms: "\f298"; -$fa-var-wrench: "\f0ad"; -$fa-var-xing: "\f168"; -$fa-var-xing-square: "\f169"; -$fa-var-y-combinator: "\f23b"; -$fa-var-y-combinator-square: "\f1d4"; -$fa-var-yahoo: "\f19e"; -$fa-var-yc: "\f23b"; -$fa-var-yc-square: "\f1d4"; -$fa-var-yelp: "\f1e9"; -$fa-var-yen: "\f157"; -$fa-var-yoast: "\f2b1"; -$fa-var-youtube: "\f167"; -$fa-var-youtube-play: "\f16a"; -$fa-var-youtube-square: "\f166"; - diff --git a/resources/public/font-awesome-4.7.0/scss/font-awesome.scss b/resources/public/font-awesome-4.7.0/scss/font-awesome.scss deleted file mode 100644 index f1c83aaa5..000000000 --- a/resources/public/font-awesome-4.7.0/scss/font-awesome.scss +++ /dev/null @@ -1,18 +0,0 @@ -/*! - * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome - * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) - */ - -@import "variables"; -@import "mixins"; -@import "path"; -@import "core"; -@import "larger"; -@import "fixed-width"; -@import "list"; -@import "bordered-pulled"; -@import "animated"; -@import "rotated-flipped"; -@import "stacked"; -@import "icons"; -@import "screen-reader"; diff --git a/resources/public/google18035ff9b05e149d.html b/resources/public/google18035ff9b05e149d.html deleted file mode 100644 index fcc0f984e..000000000 --- a/resources/public/google18035ff9b05e149d.html +++ /dev/null @@ -1 +0,0 @@ -google-site-verification: google18035ff9b05e149d.html \ No newline at end of file diff --git a/resources/public/image/anatomy.svg b/resources/public/image/anatomy.svg new file mode 100644 index 000000000..991a2bc43 --- /dev/null +++ b/resources/public/image/anatomy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/public/image/black/cursed-star.svg b/resources/public/image/black/cursed-star.svg new file mode 100644 index 000000000..a4bb31cc4 --- /dev/null +++ b/resources/public/image/black/cursed-star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/public/image/black/rod-of-asclepius.svg b/resources/public/image/black/rod-of-asclepius.svg new file mode 100644 index 000000000..e88075973 --- /dev/null +++ b/resources/public/image/black/rod-of-asclepius.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/public/image/card-logo.png b/resources/public/image/card-logo.png new file mode 100644 index 000000000..b6028c79a Binary files /dev/null and b/resources/public/image/card-logo.png differ diff --git a/resources/public/image/cursed-star.svg b/resources/public/image/cursed-star.svg new file mode 100644 index 000000000..7fc86fb82 --- /dev/null +++ b/resources/public/image/cursed-star.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/public/image/dmv-box-logo.png b/resources/public/image/dmv-box-logo.png new file mode 100644 index 000000000..ec6c9aed3 Binary files /dev/null and b/resources/public/image/dmv-box-logo.png differ diff --git a/resources/public/image/dmv-logo.svg b/resources/public/image/dmv-logo.svg new file mode 100644 index 000000000..4a2b464d6 --- /dev/null +++ b/resources/public/image/dmv-logo.svg @@ -0,0 +1,644 @@ + + + + + + image/svg+xml + + Page 1 + + + + + + Page 1 + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/public/image/header-background.jpg b/resources/public/image/header-background.jpg index dccb87132..9fa379da1 100644 Binary files a/resources/public/image/header-background.jpg and b/resources/public/image/header-background.jpg differ diff --git a/resources/public/image/login-side.jpg b/resources/public/image/login-side.jpg new file mode 100644 index 000000000..3ea03361d Binary files /dev/null and b/resources/public/image/login-side.jpg differ diff --git a/resources/public/image/orcpub-card-logo.png b/resources/public/image/orcpub-card-logo - original.png similarity index 100% rename from resources/public/image/orcpub-card-logo.png rename to resources/public/image/orcpub-card-logo - original.png diff --git a/resources/public/image/orcpub-card-logo.jpg b/resources/public/image/orcpub-card-logo.jpg deleted file mode 100644 index 00fde68a0..000000000 Binary files a/resources/public/image/orcpub-card-logo.jpg and /dev/null differ diff --git a/resources/public/image/rod-of-asclepius.svg b/resources/public/image/rod-of-asclepius.svg new file mode 100644 index 000000000..6c7a8d300 --- /dev/null +++ b/resources/public/image/rod-of-asclepius.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/public/image/shutterstock_432001912-orginal.jpg b/resources/public/image/shutterstock_432001912-orginal.jpg new file mode 100644 index 000000000..983b6e24c Binary files /dev/null and b/resources/public/image/shutterstock_432001912-orginal.jpg differ diff --git a/resources/public/image/shutterstock_432001912.jpg b/resources/public/image/shutterstock_432001912.jpg index 983b6e24c..7d64325a2 100644 Binary files a/resources/public/image/shutterstock_432001912.jpg and b/resources/public/image/shutterstock_432001912.jpg differ diff --git a/resources/public/image/spiked-dragon-head.svg b/resources/public/image/spiked-dragon-head.svg new file mode 100644 index 000000000..035c2d87c --- /dev/null +++ b/resources/public/image/spiked-dragon-head.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/public/image/spiral-orignal.gif b/resources/public/image/spiral-orignal.gif new file mode 100644 index 000000000..e784f3b56 Binary files /dev/null and b/resources/public/image/spiral-orignal.gif differ diff --git a/resources/public/image/spiral.gif b/resources/public/image/spiral.gif index e784f3b56..297ca668a 100644 Binary files a/resources/public/image/spiral.gif and b/resources/public/image/spiral.gif differ diff --git a/resources/public/index.html b/resources/public/index.html deleted file mode 100644 index e2b611f78..000000000 --- a/resources/public/index.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - The New OrcPub: D&D 5e Character Builder/Generator - - - - - -
-
- -
-
- - - - - - - - diff --git a/resources/public/js/cookies.js b/resources/public/js/cookies.js new file mode 100644 index 000000000..c0210a203 --- /dev/null +++ b/resources/public/js/cookies.js @@ -0,0 +1,93 @@ +function Pop() { + var cssRuleFile = "/css/cookiestyles.css"; + let lnk = document.createElement("link"); + lnk.setAttribute("rel", "stylesheet"); + lnk.setAttribute("type", "text/css"); + lnk.setAttribute("href", cssRuleFile); + document.getElementsByTagName("head")[0].appendChild(lnk); + let styl = "undefined"; + var conDivObj; + var fadeInTime = 10; + var fadeOutTime = 10; + let cookie = { name: "cookieconsent_status", path: "/", expiryDays: 365 * 24 * 60 * 60 * 5000 }; + let content = { message: "This website uses cookies to ensure you get the best experience on our website.", btnText: "Got it!", mode: " banner bottom", theme: " theme-classic", palette: " palette1", link: "Learn more", href: "https://www.cookiesandyou.com", target: "_blank" }; + let createPopUp = function() { + if (typeof conDivObj === "undefined") { + conDivObj = document.createElement("DIV"); + conDivObj.style.opacity = 0; + conDivObj.setAttribute("id", "cookie-policy-popup"); + } + conDivObj.innerHTML = '"; + document.body.appendChild(conDivObj); + fadeIn(conDivObj); + document.getElementById("cookie-btn").addEventListener("click", function() { + saveCookie(); + fadeOut(conDivObj); + }); + }; + let fadeOut = function(element) { + var op = 1; + var timer = setInterval(function() { + if (op <= 0.1) { + clearInterval(timer); + conDivObj.parentElement.removeChild(conDivObj); + } + element.style.opacity = op; + element.style.filter = "alpha(opacity=" + op * 100 + ")"; + op -= op * 0.1; + }, fadeOutTime); + }; + let fadeIn = function(element) { + var op = 0.1; + var timer = setInterval(function() { + if (op >= 1) { clearInterval(timer); } + element.style.opacity = op; + element.style.filter = "alpha(opacity=" + op * 100 + ")"; + op += op * 0.1; + }, fadeInTime); + }; + let checkCookie = function(key) { var keyValue = document.cookie.match("(^|;) ?" + key + "=([^;]*)(;|$)"); return keyValue ? true : false; }; + let saveCookie = function() { + var expires = new Date(); + expires.setTime(expires.getTime() + cookie.expiryDays); + document.cookie = cookie.name + + "=" + + "ok" + + ";expires=" + + expires.toUTCString() + + "path=" + + cookie.path; + }; + this.init = function(param) { + if (checkCookie(cookie.name)) return; + if (typeof param === "object") { + if ("ButtonText" in param) content.btnText = param.ButtonText; + if ("Mode" in param) content.mode = " " + param.Mode; + if ("Theme" in param) content.theme = " " + param.Theme; + if ("Palette" in param) content.palette = " " + param.Palette; + if ("Message" in param) content.message = param.Message; + if ("LinkText" in param) content.link = param.LinkText; + if ("Location" in param) content.href = param.Location; + if ("Target" in param) content.target = param.Target; + if ("Time" in param) + setTimeout(function() { createPopUp(); }, param.Time * 1000); + else createPopUp(); + } + }; +} +window.start = new Pop(); \ No newline at end of file diff --git a/resources/public/robots.txt b/resources/public/robots.txt new file mode 100644 index 000000000..77470cb39 --- /dev/null +++ b/resources/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: / \ No newline at end of file diff --git a/scripts/fix-missing-else.py b/scripts/fix-missing-else.py new file mode 100644 index 000000000..01fb4c47f --- /dev/null +++ b/scripts/fix-missing-else.py @@ -0,0 +1,93 @@ +#!/usr/bin/env python3 +""" +Fix clj-kondo 'Missing else branch' warnings by replacing: + if -> when + if-let -> when-let + if-not -> when-not + +Reads kondo output from stdin, applies fixes to source files. +Usage: lein lint 2>&1 | grep 'Missing else branch' | python3 scripts/fix-missing-else.py +""" + +import sys +import re +from collections import defaultdict + +def parse_warnings(lines): + """Parse kondo output into {file: [(line, col), ...]} sorted by line desc.""" + by_file = defaultdict(list) + for line in lines: + # Format: src/clj/foo.clj:42:3: warning: Missing else branch. + m = re.match(r'^(.+?):(\d+):(\d+):', line.strip()) + if m: + by_file[m.group(1)].append((int(m.group(2)), int(m.group(3)))) + # Sort each file's locations by line descending (process bottom-up to avoid offset shifts) + for f in by_file: + by_file[f].sort(reverse=True) + return by_file + +def fix_at(source_line, col): + """Replace if/if-let/if-not with when/when-let/when-not at the given column (1-based).""" + idx = col - 1 # convert to 0-based + + # kondo column points to '(' — the 'if' token starts at idx+1 + if source_line[idx:idx+1] == '(': + idx += 1 + + rest = source_line[idx:] + + if rest.startswith('if-let'): + return source_line[:idx] + 'when-let' + source_line[idx + 6:] + elif rest.startswith('if-not'): + return source_line[:idx] + 'when-not' + source_line[idx + 6:] + elif rest.startswith('if-some'): + return source_line[:idx] + 'when-some' + source_line[idx + 7:] + elif rest.startswith('if-first'): + return source_line[:idx] + 'when-first' + source_line[idx + 8:] + elif rest.startswith('if'): + # Make sure it's standalone 'if', not 'if-' something else + after = source_line[idx + 2:idx + 3] if len(source_line) > idx + 2 else '' + if after in (' ', '\t', '\n', ''): + return source_line[:idx] + 'when' + source_line[idx + 2:] + else: + print(f" SKIP: unexpected token at col {col}: {rest[:20]}", file=sys.stderr) + return source_line + else: + print(f" SKIP: no if at col {col}: {rest[:20]}", file=sys.stderr) + return source_line + +def main(): + warnings = parse_warnings(sys.stdin) + total_fixed = 0 + total_skipped = 0 + + for filepath, locations in sorted(warnings.items()): + with open(filepath, 'r') as f: + lines = f.readlines() + + fixed = 0 + for line_num, col in locations: + if line_num > len(lines): + print(f" SKIP: {filepath}:{line_num} beyond file length", file=sys.stderr) + total_skipped += 1 + continue + + old = lines[line_num - 1] + new = fix_at(old, col) + if old != new: + lines[line_num - 1] = new + fixed += 1 + else: + total_skipped += 1 + + if fixed > 0: + with open(filepath, 'w') as f: + f.writelines(lines) + + total_fixed += fixed + print(f" {filepath}: {fixed} fixed" + (f", {len(locations) - fixed} skipped" if len(locations) > fixed else "")) + + print(f"\nTotal: {total_fixed} fixed, {total_skipped} skipped") + +if __name__ == '__main__': + main() diff --git a/src/clj/orcpub/datomic.clj b/src/clj/orcpub/datomic.clj index 460fbc5bf..188942780 100644 --- a/src/clj/orcpub/datomic.clj +++ b/src/clj/orcpub/datomic.clj @@ -1,14 +1,49 @@ (ns orcpub.datomic + "Datomic database component with connection management and error handling. + + Provides a component that manages the database connection lifecycle, + including database creation, connection establishment, and schema initialization. + All operations include error handling with clear error messages." (:require [com.stuartsierra.component :as component] - [datomic.api :as d])) + [datomic.api :as d] + [orcpub.db.schema :as schema])) (defrecord DatomicComponent [uri conn] component/Lifecycle (start [this] (if (:conn this) this - (do - (assoc this :conn (d/connect uri))))) + (try + (when (nil? uri) + (throw (ex-info "Database URI is required but not configured" + {:error :missing-db-uri}))) + + (println "Creating/connecting to Datomic database:" uri) + (d/create-database uri) + + (let [connection (try + (d/connect uri) + (catch Exception e + (throw (ex-info "Failed to connect to Datomic database. Please verify the database URI and that Datomic is running." + {:error :db-connection-failed + :uri uri} + e))))] + (try + @(d/transact connection schema/all-schemas) + (println "Successfully initialized database schema") + (catch Exception e + (throw (ex-info "Failed to initialize database schema. The database may be in an inconsistent state." + {:error :schema-initialization-failed + :uri uri} + e)))) + (assoc this :conn connection)) + (catch clojure.lang.ExceptionInfo e + (throw e)) + (catch Exception e + (throw (ex-info "Unexpected error during database initialization" + {:error :db-init-failed + :uri uri} + e)))))) (stop [this] (assoc this :conn nil))) diff --git a/src/clj/orcpub/db/schema.clj b/src/clj/orcpub/db/schema.clj index 1b4e88553..06ed3c833 100644 --- a/src/clj/orcpub/db/schema.clj +++ b/src/clj/orcpub/db/schema.clj @@ -4,6 +4,7 @@ [orcpub.dnd.e5.character :as char5e] [orcpub.dnd.e5.units :as units5e] [orcpub.dnd.e5.party :as party5e] + [orcpub.dnd.e5.folder :as folder5e] [orcpub.dnd.e5.magic-items :as mi5e] [orcpub.dnd.e5.weapons :as weapon5e] [orcpub.dnd.e5.spells :as spells5e] @@ -21,12 +22,6 @@ :db/cardinality :db.cardinality/one :db/noHistory true}) -(defn fulltext-prop [key] - {:db/ident key - :db/valueType :db.type/string - :db/cardinality :db.cardinality/one - :db/fulltext true}) - (defn fulltext-prop [key] {:db/ident key :db/valueType :db.type/string @@ -150,6 +145,9 @@ {:db/ident :orcpub.user/password-reset-key :db/valueType :db.type/string :db/cardinality :db.cardinality/one} + {:db/ident :orcpub.user/pending-email + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} {:db/ident :orcpub.user/following :db/valueType :db.type/ref :db/cardinality :db.cardinality/many}]) @@ -318,6 +316,17 @@ :db/valueType :db.type/ref :db/cardinality :db.cardinality/many}]) +(def folder-schema + [{:db/ident ::folder5e/owner + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} + {:db/ident ::folder5e/name + :db/valueType :db.type/string + :db/cardinality :db.cardinality/one} + {:db/ident ::folder5e/character-ids + :db/valueType :db.type/ref + :db/cardinality :db.cardinality/many}]) + (def character-equipment-schema (concat [(string-prop ::char-equip-5e/name) @@ -375,9 +384,11 @@ (map bool-prop-no-history [::weapon5e/special? + ::weapon5e/loading? ::weapon5e/melee? ::weapon5e/ranged? ::weapon5e/heavy? + ::weapon5e/light? ::weapon5e/thrown? ::weapon5e/two-handed? ::weapon5e/finesse? @@ -393,5 +404,6 @@ character-equipment-schema features-used-schema party-schema + folder-schema magic-item-schema weapon-schema)) diff --git a/src/clj/orcpub/email.clj b/src/clj/orcpub/email.clj index 0acad564c..de6e33331 100644 --- a/src/clj/orcpub/email.clj +++ b/src/clj/orcpub/email.clj @@ -1,15 +1,23 @@ (ns orcpub.email + "Email sending functionality with error handling. + + Provides functions for sending verification emails, password reset emails, + and error notification emails. All operations include comprehensive error + handling to prevent silent failures when the SMTP server is unavailable." (:require [hiccup.core :as hiccup] [postal.core :as postal] [environ.core :as environ] - [orcpub.route-map :as routes])) + [clojure.pprint :as pprint] + [clojure.string :as s] + [orcpub.route-map :as routes] + [cuerdas.core :as str])) (defn verification-email-html [first-and-last-name username verification-url] [:div - (str "Dear OrcPub Patron,") + (str "Dear Dungeon Master's Vault Patron,") [:br] [:br] - "Your OrcPub account is almost ready, we just need you to verify your email address going the following URL to confirm that you are authorized to use this email address:" + "Your Dungeon Master's Vault account is almost ready, we just need you to verify your email address going the following URL to confirm that you are authorized to use this email address:" [:br] [:br] [:a {:href verification-url} verification-url] @@ -18,31 +26,106 @@ "Sincerely," [:br] [:br] - "The OrcPub Team"]) + "The Dungeon Master's Vault Team"]) (defn verification-email [first-and-last-name username verification-url] [{:type "text/html" :content (hiccup/html (verification-email-html first-and-last-name username verification-url))}]) +(defn email-change-verification-html + "Email body for existing users changing their email (distinct from registration)." + [username verification-url] + [:div + "Dear Dungeon Master's Vault Patron," + [:br] + [:br] + "You requested to change the email address on your account (" username "). " + "Please visit the following URL to confirm this change:" + [:br] + [:br] + [:a {:href verification-url} verification-url] + [:br] + [:br] + "If you did not request this change, you can safely ignore this email." + [:br] + [:br] + "Sincerely," + [:br] + [:br] + "The Dungeon Master's Vault Team"]) + +(defn email-change-verification-email [username verification-url] + [{:type "text/html" + :content (hiccup/html (email-change-verification-html username verification-url))}]) + (defn email-cfg [] - {:user (environ/env :email-access-key) - :pass (environ/env :email-secret-key) - :host "email-smtp.us-west-2.amazonaws.com" - :port 587}) + (try + {:user (environ/env :email-access-key) + :pass (environ/env :email-secret-key) + :host (environ/env :email-server-url) + :port (Integer/parseInt (or (environ/env :email-server-port) "587")) + :ssl (or (str/to-bool (environ/env :email-ssl)) nil) + :tls (or (str/to-bool (environ/env :email-tls)) nil)} + (catch NumberFormatException e + (throw (ex-info "Invalid email server port configuration. Expected a number." + {:error :invalid-port + :port (environ/env :email-server-port)} + e))))) + +(defn emailfrom [] + (if (not (s/blank? (environ/env :email-from-address))) (environ/env :email-from-address) "no-reply@dungeonmastersvault.com")) + +(defn send-verification-email + "Sends account verification email to a new user. + + Args: + base-url - Base URL for the application (for verification link) + user-map - Map containing :email, :username, and :first-and-last-name + verification-key - Unique key for email verification -(defn send-verification-email [base-url {:keys [email username first-and-last-name]} verification-key] + Returns: + Postal send-message result + + Throws: + ExceptionInfo with :verification-email-failed error code if email cannot be sent" + [base-url {:keys [email username first-and-last-name]} verification-key] + (try + (let [result (postal/send-message (email-cfg) + {:from (str "Dungeon Master's Vault Team <" (emailfrom) ">") + :to email + :subject "Dungeon Master's Vault Email Verification" + :body (verification-email + first-and-last-name + username + (str base-url (routes/path-for routes/verify-route) "?key=" verification-key))})] + (when (not= :SUCCESS (:error result)) + (throw (ex-info "Failed to send verification email" + {:error :email-send-failed + :email email + :postal-response result}))) + result) + (catch Exception e + (println "ERROR: Failed to send verification email to" email ":" (.getMessage e)) + (throw (ex-info "Unable to send verification email. Please check your email configuration or try again later." + {:error :verification-email-failed + :email email + :username username} + e))))) + +(defn send-email-change-verification + "Send a verification email for an email-change request (not registration)." + [base-url {:keys [email username]} verification-key] (postal/send-message (email-cfg) - {:from "OrcPub Team " + {:from (str "Dungeon Master's Vault Team <" (emailfrom) ">") :to email - :subject "OrcPub Email Verification" - :body (verification-email - first-and-last-name + :subject "Dungeon Master's Vault Email Change Verification" + :body (email-change-verification-email username (str base-url (routes/path-for routes/verify-route) "?key=" verification-key))})) (defn reset-password-email-html [first-and-last-name reset-url] [:div - (str "Dear OrcPub Patron") + (str "Dear Dungeon Master's Vault Patron") [:br] [:br] "We received a request to reset your password, to do so please go to the following URL to complete the reset." @@ -57,28 +140,76 @@ "Sincerely," [:br] [:br] - "The OrcPub Team"]) + "The Dungeon Master's Vault Team"]) (defn reset-password-email [first-and-last-name reset-url] [{:type "text/html" :content (hiccup/html (reset-password-email-html first-and-last-name reset-url))}]) -(defn send-reset-email [base-url {:keys [email username first-and-last-name]} reset-key] - (postal/send-message (email-cfg) - {:from "OrcPub Team " - :to email - :subject "OrcPub Password Reset" - :body (reset-password-email - first-and-last-name - (str base-url (routes/path-for routes/reset-password-page-route) "?key=" reset-key))})) +(defn send-reset-email + "Sends password reset email to a user. -(defn send-error-email [context exception] - (postal/send-message (email-cfg) - {:from "OrcPub Errors " - :to "redorc@orcpub.com" - :subject "Exception" - :body [{:type "text/plain" - :content (let [writer (java.io.StringWriter.)] - (do (clojure.pprint/pprint (:request context) writer) - (clojure.pprint/pprint (or (ex-data exception) exception) writer) - (str writer)))}]})) + Args: + base-url - Base URL for the application (for reset link) + user-map - Map containing :email, :username, and :first-and-last-name + reset-key - Unique key for password reset + + Returns: + Postal send-message result + + Throws: + ExceptionInfo with :reset-email-failed error code if email cannot be sent" + [base-url {:keys [email username first-and-last-name]} reset-key] + (try + (let [result (postal/send-message (email-cfg) + {:from (str "Dungeon Master's Vault Team <" (emailfrom) ">") + :to email + :subject "Dungeon Master's Vault Password Reset" + :body (reset-password-email + first-and-last-name + (str base-url (routes/path-for routes/reset-password-page-route) "?key=" reset-key))})] + (when (not= :SUCCESS (:error result)) + (throw (ex-info "Failed to send password reset email" + {:error :email-send-failed + :email email + :postal-response result}))) + result) + (catch Exception e + (println "ERROR: Failed to send password reset email to" email ":" (.getMessage e)) + (throw (ex-info "Unable to send password reset email. Please check your email configuration or try again later." + {:error :reset-email-failed + :email email + :username username} + e))))) + +(defn send-error-email + "Sends error notification email to configured admin email. + + This function is called when unhandled exceptions occur in the application. + It includes request context and exception details for debugging. + + Args: + context - Request context map + exception - The exception that occurred + + Returns: + Postal send-message result, or nil if no error email is configured + or if sending fails (failures are logged but not thrown)" + [context exception] + (when (not-empty (environ/env :email-errors-to)) + (try + (let [result (postal/send-message (email-cfg) + {:from (str "Dungeon Master's Vault Errors <" (emailfrom) ">") + :to (str (environ/env :email-errors-to)) + :subject "Exception" + :body [{:type "text/plain" + :content (let [writer (java.io.StringWriter.)] + (clojure.pprint/pprint (:request context) writer) + (clojure.pprint/pprint (or (ex-data exception) exception) writer) + (str writer))}]})] + (when (not= :SUCCESS (:error result)) + (println "WARNING: Failed to send error notification email:" (:error result))) + result) + (catch Exception e + (println "ERROR: Failed to send error notification email:" (.getMessage e)) + nil)))) \ No newline at end of file diff --git a/src/clj/orcpub/favicon.clj b/src/clj/orcpub/favicon.clj new file mode 100644 index 000000000..7bf3a09ac --- /dev/null +++ b/src/clj/orcpub/favicon.clj @@ -0,0 +1,26 @@ +(ns orcpub.favicon + (:require [hiccup.def :refer [defhtml]])) + +(defhtml install + "faviconit.com favicons. + Filenames default to: icon.ico; browserconfig.xml; + png - 16,32,57,64,72,76,96,114,120,144,152,160,196," + [& {:keys [img xml png-prefix ver]}] + [:link {:rel "shortcut icon" :href (str img "/favicon.ico?v=" ver)}] + [:link {:rel "icon" :sizes "16x16 32x32 64x64" :href (str img "/favicon.ico?v=" ver)}] + [:link {:rel "icon" :type "image/png" :sizes "196x196" :href (str img "/" png-prefix "196.png?v=" ver)}] + [:link {:rel "icon" :type "image/png" :sizes "160x160" :href (str img "/" png-prefix "160.png?v=" ver)}] + [:link {:rel "icon" :type "image/png" :sizes "96x96" :href (str img "/" png-prefix "96.png?v=" ver)}] + [:link {:rel "icon" :type "image/png" :sizes "64x64" :href (str img "/" png-prefix "64.png?v=" ver)}] + [:link {:rel "icon" :type "image/png" :sizes "32x32" :href (str img "/" png-prefix "32.png?v=" ver)}] + [:link {:rel "icon" :type "image/png" :sizes "16x16" :href (str img "/" png-prefix "16.png?v=" ver)}] + [:link {:rel "apple-touch-icon" :sizes "152x152" :href (str img "/" png-prefix "152.png?v=" ver)}] + [:link {:rel "apple-touch-icon" :sizes "144x144" :href (str img "/" png-prefix "144.png?v=" ver)}] + [:link {:rel "apple-touch-icon" :sizes "120x120" :href (str img "/" png-prefix "120.png?v=" ver)}] + [:link {:rel "apple-touch-icon" :sizes "114x114" :href (str img "/" png-prefix "114.png?v=" ver)}] + [:link {:rel "apple-touch-icon" :sizes "76x76" :href (str img "/" png-prefix "76.png?v=" ver)}] + [:link {:rel "apple-touch-icon" :sizes "72x72" :href (str img "/" png-prefix "72.png?v=" ver)}] + [:link {:rel "apple-touch-icon" :href (str img "/" png-prefix "57.png?v=" ver)}] + [:meta {:name "msapplication-TileColor" :content "#FFFFFF"}] + [:meta {:name "msapplication-TileImage" :content (str img "/" png-prefix "144.png?v=" ver)}] + [:meta {:name "msapplication-config" :content (str xml "/browserconfig.xml")}]) diff --git a/src/clj/orcpub/index.clj b/src/clj/orcpub/index.clj index c341a4557..c7baa01cb 100644 --- a/src/clj/orcpub/index.clj +++ b/src/clj/orcpub/index.clj @@ -1,10 +1,15 @@ (ns orcpub.index (:require [hiccup.page :refer [html5 include-css include-js]] [orcpub.oauth :as oauth] - [orcpub.dnd.e5.views-2 :as views-2])) + [orcpub.dnd.e5.views-2 :as views-2] + [orcpub.favicon :as fi] + [environ.core :refer [env]] + )) + +(def devmode? (env :dev-mode)) (defn meta-tag [property content] - (if content + (when content [:meta {:property property :content content}])) @@ -16,18 +21,24 @@ fb-type]} & [splash?]] (html5 - {:lang :en - :style "height:100%"} + {:lang :en} [:head (meta-tag "og:url" url) (meta-tag "og:type" fb-type) (meta-tag "og:title" title) (meta-tag "og:description" description) (meta-tag "og:image" image) - (meta-tag "google-signin-client_id" "86323071944-te5j96nbke0duomgm24j2on4rs4p7ob9.apps.googleusercontent.com") [:meta {:charset "UTF-8"}] [:meta {:name "viewport" - :content "width=device-width, initial-scale=1"}] + :content "width=device-width, initial-scale=1.0, minimum-scale=1.0"}] + (fi/install :png-prefix "favicon-" + :img "/favicon" + :xml "/favicon" + :ver "1") + (include-css "/css/cookiestyles.css") + [:script + "document.documentElement.style.setProperty('--innerHeight', `${window.innerHeight}px`); + window.addEventListener('resize', () => document.documentElement.style.setProperty('--innerHeight', `${window.innerHeight}px`));"] [:style " .splash-page-content {} @@ -40,9 +51,15 @@ .splash-button .splash-button-content {height: 60px; width: 60px; font-size: 10px} .legal-footer-parent {display: none}} -#app {height:100%;background-image: linear-gradient(182deg, #313A4D, #080A0D)} +body {background-color: #080A0D} + +#app {background-image: linear-gradient(182deg, #313A4D, #080A0D);background-attachment: fixed} + +.app {height:100%;font-family:Open Sans, sans-serif} -.app {background-image: linear-gradient(182deg, #313A4D, #080A0D);height:100%;overflow-y:scroll;-webkit-overflow-scrolling :touch;font-family:Open Sans, sans-serif} +.h-full {height: 100vh;height: var(--innerHeight, 100vh)} + +.min-h-full {min-height: 100vh;min-height: var(--innerHeight, 100vh)} html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, @@ -95,46 +112,52 @@ table { border-spacing: 0; } -html, body, #app { - height: 100%; +html { + min-height: 100%; }"] - [:title title] - [:script - (format - " window.fbAsyncInit = function() { - FB.init({ - appId : '%s', - xfbml : true, - cookie : true, - version : 'v2.9' - }); - FB.AppEvents.logPageView(); - }; - - (function(d, s, id){ - var js, fjs = d.getElementsByTagName(s)[0]; - if (d.getElementById(id)) {return;} - js = d.createElement(s); js.id = id; - js.src = \"//connect.facebook.net/en_US/sdk.js\"; - fjs.parentNode.insertBefore(js, fjs); - }(document, 'script', 'facebook-jssdk'));" - (oauth/app-id url))]] - [:body {:style "margin:0;height:100%;line-height:1"} + [:title title]] + [:body {:style "margin:0;line-height:1"} [:div#app (if splash? (views-2/splash-page) - [:div {:style "display:flex;justify-content:space-around"} + [:div.h-full {:style "display:flex;justify-content:space-around"} [:img {:src "/image/spiral.gif" :style "height:200px;width:200px;margin-top:200px"}]])] (include-css "/css/compiled/styles.css") (include-js "/js/compiled/orcpub.js") - (include-css "/font-awesome-4.7.0/css/font-awesome.min.css") + (include-js "/js/cookies.js") + (include-css "/assets/font-awesome/5.13.1/css/all.min.css") (include-css "https://fonts.googleapis.com/css?family=Open+Sans") - [:script - "(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ - (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), - m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) - })(window,document,'script','https://www.google-analytics.com/analytics.js','ga'); + [:script " window.start.init({Palette:\"palette7\",Mode:\"banner bottom\",})"] + (if devmode? + (println "dev mode - no script") - ga('create', 'UA-69209720-3', 'auto'); - ga('send', 'pageview');"]])) + [:script + "const protocol = window.location.protocol; + const apiUrl = `${protocol}//${window.location.host}`; + const pluginUrl = `${apiUrl}/homebrew.orcbrew`; + + let plugins = localStorage.getItem('plugins'); + if (plugins === null || plugins === '{}') { + fetch(pluginUrl) + .then(resp => { + if (!resp.ok) { + throw new Error(`Failed to fetch plugins: ${resp.status} ${resp.statusText}`); + } + return resp.text(); + }) + .then(text => { + if (!text.toUpperCase().includes('NOT FOUND')) { + localStorage.setItem('plugins', text); + window.location.reload(false); + } + }) + .catch(error => { + console.error('Error fetching plugins:', error); + // You can also add a fallback or default behavior here + }); + } + "] + ) + ])) + diff --git a/src/clj/orcpub/oauth.clj b/src/clj/orcpub/oauth.clj index c866cc731..ae58ccb31 100644 --- a/src/clj/orcpub/oauth.clj +++ b/src/clj/orcpub/oauth.clj @@ -3,77 +3,8 @@ [cheshire.core :as json] [orcpub.route-map :as route-map])) -(def google-client-id "753086155056-fp9804v86fbf1vk3t4i0ql8polalcsn7.apps.googleusercontent.com") -(def google-client-secret "xiQ691JP9S89OO5uKVmIt1as") -(def google-oauth-url (str "https://accounts.google.com/o/oauth2/v2/auth?client_id=" google-client-id "&response_type=code&scope=email&redirect_uri=")) -(def google-token-url "https://www.googleapis.com/oauth2/v4/token") - -(def fb-client-id "1673290702980265") -(def fb-client-secret "2377805fe291bbd9a40ee3d42ee46b90") -(def fb-oauth-url (str "https://www.facebook.com/dialog/oauth?client_id=" fb-client-id "&scope=email&redirect_uri=")) -(def fb-token-url "https://graph.facebook.com/v2.3/oauth/access_token?") - -(defn app-id [url] - (if (clojure.string/starts-with? url "http://localhost") - "1994900940729588" - fb-client-id)) - (defn base-url [protocol host & [port]] (str protocol "//" host (when port (str ":" port)))) (defn get-base-url [req] - (base-url (str (name (:scheme req)) ":") ((:headers req) "host"))) - -(defn get-fb-redirect-uri [req] - (str (get-base-url req) (route-map/path-for route-map/fb-login-route))) - -(defn get-google-redirect-uri [req] - (str (get-base-url req) (route-map/path-for route-map/google-login-route))) - -(defn get-fb-user [access-token] - (let [resp (client/get - "https://graph.facebook.com/me" - {:query-params {:access_token access-token - :fields "email" - :debug "all"}}) - body (json/parse-string (:body resp) true)] - body)) - -(defn get-google-user [access-token] - (let [resp (client/get - "https://www.googleapis.com/oauth2/v2/userinfo" - {:query-params {:access_token access-token :fields "email"}}) - body (json/parse-string (:body resp) true)] - body)) - -(defn get-fb-access-token [code redirect-uri] - (let [resp (client/get fb-token-url - {:throw-exceptions false - :query-params - {:client_id fb-client-id - :redirect_uri redirect-uri - :client_secret fb-client-secret - :code code}}) - body (json/parse-string (:body resp) true) - access-token (:access_token body)] - access-token)) - -(defn get-google-access-token [code redirect-uri] - (let [resp (client/post google-token-url {:query-params {:client_id google-client-id :redirect_uri redirect-uri :client_secret google-client-secret :code code :grant_type "authorization_code"}}) - body (json/parse-string (:body resp) true) - access-token (:access_token body)] - access-token)) - -(defn get-fb-email [{:keys [query-params] :as request}] - (let [redirect-uri (get-fb-redirect-uri request) - access-token (get-fb-access-token (:code query-params) redirect-uri) - fb-user (get-fb-user access-token) - email (:email fb-user)] - email)) - -(defn get-google-email [{:keys [query-params] :as request}] - (let [redirect-uri (get-google-redirect-uri request) - access-token (get-google-access-token (:code query-params) redirect-uri) - google-user (get-google-user access-token) - email (:email google-user)] - email)) + (base-url (str (name (:scheme req)) ":") ((:headers req) "host"))) \ No newline at end of file diff --git a/src/clj/orcpub/pdf.clj b/src/clj/orcpub/pdf.clj index 5aa52704e..171699102 100644 --- a/src/clj/orcpub/pdf.clj +++ b/src/clj/orcpub/pdf.clj @@ -1,18 +1,15 @@ (ns orcpub.pdf (:require [clojure.string :as s] + [clojure.stacktrace :as strace] + [clojure.java.io :as io] [orcpub.common :as common] [orcpub.dnd.e5.display :as dis5e] [orcpub.dnd.e5.monsters :as monsters] - [orcpub.dnd.e5.options :as options] - [clojure.java.io :as io]) - (:import (org.apache.pdfbox.pdmodel.interactive.form PDCheckBox PDComboBox PDListBox PDRadioButton PDTextField) - (org.apache.pdfbox.cos COSName) + [orcpub.dnd.e5.options :as options]) + (:import (org.apache.pdfbox.pdmodel.interactive.form PDCheckBox PDTextField) (org.apache.pdfbox.pdmodel PDPage PDDocument PDPageContentStream PDResources) - (org.apache.pdfbox.pdmodel.graphics.image PDImageXObject) - (java.io ByteArrayOutputStream ByteArrayInputStream) (org.apache.pdfbox.pdmodel.graphics.image JPEGFactory LosslessFactory) (org.apache.pdfbox.pdmodel.font PDType1Font PDFont PDType0Font) - (org.eclipse.jetty.server.handler.gzip GzipHandler) (javax.imageio ImageIO) (java.net URL))) @@ -42,7 +39,7 @@ (let [field (.getField form (name k))] (when field - (if (and flatten (font-sizes k) (instance? PDTextField field)) + (when (and flatten (font-sizes k) (instance? PDTextField field)) (.setDefaultAppearance field (str "/Helv " " " (font-sizes k) " Tf 0 0 0 rg")) ;; this prints out weird boxes #_(.setDefaultAppearance field (str COSName/DA "/" (.getName font-name) " " (font-sizes k 8) " Tf 0 0 0 rg"))) @@ -52,7 +49,7 @@ (instance? PDCheckBox field) (if v "Yes" "Off") (instance? PDTextField field) (str v) :else nil)))) - (catch Exception e (prn "failed writing field: " k v (clojure.stacktrace/print-stack-trace e))))) + (catch Exception e (prn "failed writing field: " k v (strace/print-stack-trace e))))) (when flatten (.setNeedAppearances form false) (.flatten form)))) @@ -91,17 +88,64 @@ (in-to-sz scaled-width) (in-to-sz scaled-height)))) +(def user-agent "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.22 (KHTML, like Gecko) Chrome/25.0.1364.172") + (defn draw-non-jpg [doc page url x y width height] - (with-open [c-stream (content-stream doc page)] - (let [buff-image (ImageIO/read (.openStream (URL. url))) - img (LosslessFactory/createFromImage doc buff-image)] - (draw-imagex c-stream img x y width height)))) + (try + (with-open [c-stream (content-stream doc page)] + (let [connection (doto (.openConnection (URL. url)) + (.setRequestProperty "User-Agent" user-agent) + (.setConnectTimeout 10000) + (.setReadTimeout 10000)) + buff-image (ImageIO/read (.getInputStream connection))] + (when (nil? buff-image) + (throw (ex-info "Unable to read image from URL" + {:error :invalid-image-format + :url url}))) + (let [img (LosslessFactory/createFromImage doc buff-image)] + (draw-imagex c-stream img x y width height)))) + (catch java.net.SocketTimeoutException e + (throw (ex-info (str "Timeout loading image from URL: " url) + {:error :image-load-timeout + :url url} + e))) + (catch java.net.UnknownHostException e + (throw (ex-info (str "Unable to resolve host for image URL: " url) + {:error :unknown-host + :url url} + e))) + (catch Exception e + (throw (ex-info (str "Failed to load image from URL: " url) + {:error :image-load-failed + :url url} + e))))) (defn draw-jpg [doc page url x y width height] - (with-open [c-stream (content-stream doc page) - image-stream (.openStream (URL. url))] - (let [img (JPEGFactory/createFromStream doc image-stream)] - (draw-imagex c-stream img x y width height)))) + (try + (with-open [c-stream (content-stream doc page) + image-stream (.getInputStream + (doto + (.openConnection (URL. url)) + (.setRequestProperty "User-Agent" user-agent) + (.setConnectTimeout 10000) + (.setReadTimeout 10000)))] + (let [img (JPEGFactory/createFromStream doc image-stream)] + (draw-imagex c-stream img x y width height))) + (catch java.net.SocketTimeoutException e + (throw (ex-info (str "Timeout loading image from URL: " url) + {:error :image-load-timeout + :url url} + e))) + (catch java.net.UnknownHostException e + (throw (ex-info (str "Unable to resolve host for image URL: " url) + {:error :unknown-host + :url url} + e))) + (catch Exception e + (throw (ex-info (str "Failed to load JPEG image from URL: " url) + {:error :jpeg-load-failed + :url url} + e))))) (defn draw-image! [doc page url x y width height] (let [lower-case-url (s/lower-case url) @@ -110,8 +154,16 @@ draw-fn (if jpg? draw-jpg draw-non-jpg)] (try (draw-fn doc page url x y width height) + (catch clojure.lang.ExceptionInfo e + (println "ERROR: Failed to load image for PDF:" (.getMessage e)) + (println " URL:" url) + (println " Details:" (ex-data e)) + nil) (catch Exception e - (prn "failed loading image" (clojure.stacktrace/print-stack-trace e)))))) + (println "ERROR: Unexpected error loading image for PDF:" (.getMessage e)) + (println " URL:" url) + (clojure.stacktrace/print-stack-trace e) + nil)))) (defn get-page [doc index] (.getPage doc index)) @@ -127,7 +179,7 @@ current-line nil [next-word & remaining-words :as current-words] words] (if next-word - (let [line-with-word (str current-line (if current-line " ") next-word) + (let [line-with-word (str current-line (when current-line " ") next-word) new-width (string-width line-with-word font font-size)] (if (> new-width width) (recur (conj lines current-line) @@ -142,7 +194,7 @@ (defn draw-lines-to-box [cs lines font font-size x y height] (let [leading (* font-size 1.1) - max-lines (- (/ (* 72 height) leading) 1) + max-lines (dec (/ (* 72 height) leading)) units-x (* 72 x) units-y (* 72 y) fitting-lines (vec (take max-lines lines))] @@ -164,16 +216,16 @@ (.setNonStrokingColor cs r g b)) (defn draw-text [cs text font font-size x y & [color]] - (if text + (when text (let [units-x (* 72 x) units-y (* 72 y)] (.beginText cs) (.setFont cs font font-size) - (if color + (when color (apply set-text-color cs color)) (.moveTextPositionByAmount cs units-x units-y) (.drawString cs (if (keyword? text) (common/safe-name text) text)) - (if color + (when color (set-text-color cs 0 0 0)) (.endText cs)))) @@ -214,10 +266,12 @@ (+ margin-x total-width) y))) (.setStrokingColor cs 0 0 0))) -(defn spell-school-level [{:keys [level school]}] - (if (zero? level) - (str (s/capitalize school) " cantrip") - (str "Level-" level " " school))) + +(defn spell-school-level [{:keys [level school]} class-nm] + (let [school-str (if school (s/capitalize school) "Unknown")] + (if (and level (zero? level)) + (str class-nm " Cantrip " school-str) + (str class-nm " Level " (or level "?") " " school-str)))) (defn draw-spell-field [cs document title value x y] (with-open [img-stream (io/input-stream (io/resource (str "public/image/" title ".png")))] @@ -247,20 +301,20 @@ (subs s 0 len))) (defn abbreviate-duration [duration] - (if duration + (when duration (-> duration (s/replace #"Concentration,? up to " "Conc, ") abbreviate-times (s/replace #"Instantaneous.*" "Inst") - (s/replace #"round" "rnd") + (s/replace #"round" "Rnd") (max-len 16)))) (defn abbreviate-casting-time [casting-time] (-> casting-time abbreviate-times - (s/replace #"bonus action" "b.a.") - (s/replace #"action" "act.") - (s/replace #"reaction" "react."))) + (s/replace #"bonus action" "B.A.") + (s/replace #"action" "Act.") + (s/replace #"reaction" "React."))) (defn abbreviate-range [range] (-> range @@ -277,15 +331,15 @@ remaining-height (- 11.0 total-height) margin-y (/ remaining-height 2) fonts (load-fonts document)] - (with-open [img-stream (io/input-stream (io/resource "public/image/orcpub-card-logo.png")) + (with-open [img-stream (io/input-stream (io/resource "public/image/card-logo.png")) over-img-stream (io/input-stream (io/resource "public/image/clockwise-rotation.png"))] (let [img (LosslessFactory/createFromImage document (ImageIO/read img-stream)) over-img (LosslessFactory/createFromImage document (ImageIO/read over-img-stream))] (draw-grid cs 2.5 3.5) (draw-text cs (str "Page " (inc page-number) " (reverse)") - (:bold-italic fonts) - 10 + (:italic fonts) + 8 0.12 (- 11 0.15)) (doall @@ -321,7 +375,7 @@ (- box-width 0.3) (- box-height 0.2)))))))))) -(defn print-spells [cs document box-width box-height spells page-number] +(defn print-spells [cs document box-width box-height spells page-number print-spell-card-dc-mod?] (let [num-boxes-x (int (/ 8.5 box-width)) num-boxes-y (int (/ 11.0 box-height)) total-width (* num-boxes-x box-width) @@ -331,150 +385,147 @@ remaining-height (- 11.0 total-height) margin-y (/ remaining-height 2) fonts (load-fonts document)] - (with-open [img-stream (io/input-stream (io/resource "public/image/orcpub-card-logo.png")) + (with-open [card-logo-img-stream (io/input-stream (io/resource "public/image/card-logo.png")) over-img-stream (io/input-stream (io/resource "public/image/clockwise-rotation.png"))] - (let [img (LosslessFactory/createFromImage document (ImageIO/read img-stream)) + (let [card-logo-img (LosslessFactory/createFromImage document (ImageIO/read card-logo-img-stream)) over-img (LosslessFactory/createFromImage document (ImageIO/read over-img-stream))] (draw-grid cs 2.5 3.5) (draw-text cs (str "Page " (inc page-number)) - (:bold-italic fonts) - 10 + (:italic fonts) + 8 0.12 (- 11 0.15)) (doall (for [j (range num-boxes-y) i (range (dec num-boxes-x) -1 -1) :let [spell-index (+ i (* j num-boxes-x))]] - (if-let [{:keys [class-nm dc attack-bonus spell] :as spell-data} + (when-let [{:keys [class-nm dc attack-bonus spell] :as spell-data} (get (vec spells) spell-index)] - (do - (if spell - (do - (let [{:keys [description - casting-time - duration - level - range]} spell - x (+ margin-x (* box-width i)) - y (+ margin-y (* box-height j)) - - {:keys [page source description summary]} spell - - dc-str (str "DC " dc) - dc-offset (+ x 0.22 (string-width class-nm (:bold fonts) 10)) - remaining-desc-lines - (draw-text-to-box cs - (or description - (if summary - (str summary - " (see " - (if source - (s/upper-case (name source)) - "PHB") - " " - page - " for more details)") - "")) - (:plain fonts) - 8 - (+ x 0.12) - (- 11.0 y 0.65) - (- box-width 0.24) - (- box-height 0.9))] - (draw-imagex cs - img - (+ x 1.4) - (+ y 0.05) - 1.0 - 0.25) - (draw-text-to-box cs - (spell-school-level spell) - (:italic fonts) - 8 - (+ x 0.12) - (- 11.0 y 0.10) - (- box-width 0.24) - 0.25) - (draw-text-to-box cs - (:name spell) - (:bold fonts) - 10 - (+ x 0.12) - (- 11.0 y 0.27) - (- box-width 0.3) - 0.25) - (if casting-time (draw-spell-field cs - document - "magic-swirl" - (abbreviate-casting-time - (first - (s/split - casting-time - #","))) - (+ x 0.12) - (- 11.0 y 0.55))) - (if range - (draw-spell-field cs - document - "arrow-dunk" - (abbreviate-range range) - (+ x 0.62) - (- 11.0 y 0.55))) - (draw-spell-field cs - document - "shiny-purse" - (s/join - "," - (remove - nil? - (map - (fn [[k v]] - (if (-> spell :components k) - v)) - {:verbal "V" - :somatic "S" - :material "M"}))) - (+ x 1.12) - (- 11.0 y 0.55)) - (if duration - (draw-spell-field cs - document - "sands-of-time" - (abbreviate-duration duration) - (+ x 1.62) - (- 11.0 y 0.55))) - (when (not= class-nm "Homebrew") - (draw-text cs - class-nm + (let [{:keys [description + casting-time + duration + level + ritual + range]} spell + x (+ margin-x (* box-width i)) + y (+ margin-y (* box-height j)) + + {:keys [page source description summary components]} spell + ;; Handle nil spell name gracefully + spell-name (or (:name spell) "(Unknown Spell)") + + dc-str (str "DC " dc) + remaining-desc-lines + (draw-text-to-box cs + (or description + (if summary + (str summary + " (see " + (if source + (s/upper-case (name source)) + "PHB") + " " + page + " for more details)") + "")) + (:plain fonts) + 8 + (+ x 0.12) ; from the left + (- 11.0 y 1.08) ;from the top down + (- box-width 0.24) + (- box-height 1.13))] + (when (:material-component components) + (draw-text-to-box cs + (str (s/capitalize (:material-component components))) + (:italic fonts) + 8 + (+ x 0.12) + (- 11.0 y 0.55) + (- box-width 0.24) + 0.5)) + (draw-imagex cs + card-logo-img + (+ x 1.9) + (+ y 0.02) + 1.0 + 0.25) + (draw-text-to-box cs + spell-name (:bold fonts) 10 (+ x 0.12) - (- 11.0 y 3.4) - [186 21 3]) - (draw-text cs - dc-str - (:bold-italic fonts) - 8 - dc-offset - (- 11.0 y 3.4) - [186 21 3]) - (draw-text cs - (str "Mod " (common/bonus-str attack-bonus)) - (:bold-italic fonts) - 8 - (+ dc-offset (string-width dc-str (:bold fonts) 10)) - (- 11.0 y 3.4) - [186 21 3])) - (if (seq remaining-desc-lines) - (draw-imagex cs - over-img - (+ x 2.2) - (+ y 3.2) - 0.25 - 0.25)) - {:remaining-lines remaining-desc-lines - :spell-name (:name spell)}))))))))))) + (- 11.0 y) + (- box-width 0.3) + 0.2) + (draw-text-to-box cs + (if ritual " (ritual)" "") + (:italic fonts) + 10 + (+ x 0.12 (string-width spell-name (:bold fonts) 10)) + (- 11.0 y) + (- box-width 0.3) + 0.2) + (draw-text-to-box cs + (if (not= class-nm "Homebrew") + (str (spell-school-level spell class-nm) (when print-spell-card-dc-mod? (str " " dc-str " Spell Mod " (common/bonus-str attack-bonus)))) + (spell-school-level spell class-nm)) + (:italic fonts) + 8 + (+ x 0.12) + (- 11.0 y 0.19) + (- box-width 0.24) + 0.25) + (when casting-time + (draw-spell-field cs + document + "magic-swirl" + (str (abbreviate-casting-time + (first + (s/split + casting-time + #",")))) + (+ x 0.12) + (- 11.0 y 0.45))) + (when range + (draw-spell-field cs + document + "arrow-dunk" + (abbreviate-range range) + (+ x 0.62) + (- 11.0 y 0.45))) + (draw-spell-field cs + document + "shiny-purse" + (s/join + "," + (remove + nil? + (map + (fn [[k v]] + (when (-> spell :components k) + v)) + {:verbal "V" + :somatic "S" + :material "M"}))) + (+ x 1.12) + (- 11.0 y 0.45)) + (when duration + (draw-spell-field cs + document + "sands-of-time" + (abbreviate-duration duration) + (+ x 1.62) + (- 11.0 y 0.45))) + (when (seq remaining-desc-lines) + (draw-imagex cs + over-img + (+ x 2.3) + (+ y 3.3) + 0.15 + 0.15)) + {:remaining-lines remaining-desc-lines + :spell-name spell-name})))))))) (defn create-monsters-pdf [] (let [page (PDPage.) @@ -488,63 +539,63 @@ (doseq [i (range 0 5)] (let [monster (monsters i)] (draw-text-from-top cs - (:name monster) - PDType1Font/HELVETICA_BOLD - 14 - 0.1 - (+ (* i h) 0.25)) + (:name monster) + PDType1Font/HELVETICA_BOLD + 14 + 0.1 + (+ (* i h) 0.25)) (draw-text-from-top cs - (monsters/monster-subheader monster) - PDType1Font/HELVETICA_OBLIQUE - 12 - 0.1 - (+ (* i h) 0.45)) + (monsters/monster-subheader monster) + PDType1Font/HELVETICA_OBLIQUE + 12 + 0.1 + (+ (* i h) 0.45)) (doseq [j (range 0 6)] (let [ability ([:str :dex :con :int :wis :cha] j) x (+ 0.15 (* 0.65 j))] (draw-text-from-top cs - (name ability) - PDType1Font/HELVETICA_BOLD - 10 - x - (+ (* i h) 0.7)) - (draw-text-from-top cs - (str (ability monster) - " (" - (options/ability-bonus-str (ability monster)) - ")") - PDType1Font/HELVETICA - 12 - x - (+ (* i h) 0.85)))) - (draw-text-from-top cs - "Saving Throws" + (name ability) PDType1Font/HELVETICA_BOLD 10 - 0.1 - (+ (* i h) 1.1)) - (draw-text-from-top cs - (common/print-bonus-map (:saving-throws monster)) + x + (+ (* i h) 0.7)) + (draw-text-from-top cs + (str (ability monster) + " (" + (options/ability-bonus-str (ability monster)) + ")") PDType1Font/HELVETICA - 10 - (+ 0.1 (string-width - "Saving Throws " - PDType1Font/HELVETICA_BOLD - 10)) - (+ (* i h) 1.1)) + 12 + x + (+ (* i h) 0.85)))) (draw-text-from-top cs - "Skills" - PDType1Font/HELVETICA_BOLD - 10 - 0.1 - (+ (* i h) 1.3)) + "Saving Throws" + PDType1Font/HELVETICA_BOLD + 10 + 0.1 + (+ (* i h) 1.1)) (draw-text-from-top cs - (common/print-bonus-map (:skills monster)) - PDType1Font/HELVETICA - 10 - (+ 0.1 (string-width - "Skills " - PDType1Font/HELVETICA_BOLD - 10)) - (+ (* i h) 1.3))))))) + (common/print-bonus-map (:saving-throws monster)) + PDType1Font/HELVETICA + 10 + (+ 0.1 (string-width + "Saving Throws " + PDType1Font/HELVETICA_BOLD + 10)) + (+ (* i h) 1.1)) + (draw-text-from-top cs + "Skills" + PDType1Font/HELVETICA_BOLD + 10 + 0.1 + (+ (* i h) 1.3)) + (draw-text-from-top cs + (common/print-bonus-map (:skills monster)) + PDType1Font/HELVETICA + 10 + (+ 0.1 (string-width + "Skills " + PDType1Font/HELVETICA_BOLD + 10)) + (+ (* i h) 1.3))))))) (.save doc "/home/larry/Documents/test.pdf"))) \ No newline at end of file diff --git a/src/clj/orcpub/pedestal.clj b/src/clj/orcpub/pedestal.clj index f8c40525c..6461e6483 100644 --- a/src/clj/orcpub/pedestal.clj +++ b/src/clj/orcpub/pedestal.clj @@ -1,8 +1,7 @@ (ns orcpub.pedestal (:require [com.stuartsierra.component :as component] [io.pedestal.http :as http] - [pandect.algo.sha1 :refer :all] - [ring.middleware.etag :refer [wrap-etag]] + [pandect.algo.sha1 :refer [sha1]] [datomic.api :as d] [clojure.string :as s] [clj-time.format :as tf] @@ -16,9 +15,9 @@ (defn db-interceptor [conn] {:name :db-interceptor :enter (fn [context] - (let [conn (:conn conn)] - (let [db (d/db conn)] - (update context :request assoc :db db :conn conn))))}) + (let [conn (:conn conn) + db (d/db conn)] + (update context :request assoc :db db :conn conn)))}) (defmulti calculate-etag class) @@ -48,7 +47,7 @@ {body :body {last-modified "Last-Modified" content-length "Content-Length"} :headers} response - old-etag (if if-none-match + old-etag (when if-none-match (-> if-none-match (s/split #"--gzip") first)) new-etag (or (parse-date last-modified content-length) (calculate-etag body)) not-modified? (and old-etag (= new-etag old-etag))] diff --git a/src/clj/orcpub/privacy.clj b/src/clj/orcpub/privacy.clj index dc3871e7e..d4170e75d 100644 --- a/src/clj/orcpub/privacy.clj +++ b/src/clj/orcpub/privacy.clj @@ -1,5 +1,7 @@ (ns orcpub.privacy - (:require [hiccup.page :as page])) + (:require [hiccup.page :as page] + [clojure.string :as s] + [environ.core :as environ])) (defn section [{:keys [title font-size paragraphs subsections]}] [:div @@ -14,73 +16,76 @@ subsections)]) (def privacy-policy-section - {:title "Privacy Policy" + {:title "Privacy Policy" :font-size 48 :subsections - [{:title "Thank you for using OrcPub!" - :font-size 32 - :paragraphs - ["We wrote this policy to help you understand what information we collect, how we use it, and what choices you have. Because we're an internet company, some of the concepts below are a little technical, but we've tried our best to explain things in a simple and clear way. We welcome your questions and comments on this policy."]} - {:title "How We Collect Your Information" - :font-size 32 - :subsections - [{:title "When you give it to us or give us permission to obtain it" - :font-size 28 - :paragraphs - ["When you sign up for or use our products, you voluntarily give us certain information. This can include your name, profile photo, role-playing game characters, comments, likes, the email address or phone number you used to sign up, and any other information you provide us. If you're using OrcPub on your mobile device, you can also choose to provide us with location data. And if you choose to buy something on OrcPub, you provide us with payment information, contact information (ex., address and phone number), and what you purchased. If you buy something for someone else on OrcPub, you'd also provide us with their shipping details and contact information." - "You also may give us permission to access your information in other services. For example, you may link your Facebook or Twitter account to OrcPub, which allows us to obtain information from those accounts (like your friends or contacts). The information we get from those services often depends on your settings or their privacy policies, so be sure to check what those are."]} - {:title "We also get technical information when you use our products" - :font-size 28 - :paragraphs - ["These days, whenever you use a website, mobile application, or other internet service, there's certain information that almost always gets created and recorded automatically. The same is true when you use our products. Here are some of the types of information we collect:" - "Log data. When you use OrcPub, our servers may automatically record information (\"log data\"), including information that your browser sends whenever you visit a website or your mobile app sends when you're using it. This log data may include your Internet Protocol address, the address of the web pages you visited that had OrcPub features, browser type and settings, the date and time of your request, how you used OrcPub, and cookie data." - "Cookie data. Depending on how you're accessing our products, we may use \"cookies\" (small text files sent by your computer each time you visit our website, unique to your OrcPub account or your browser) or similar technologies to record log data. When we use cookies, we may use \"session\" cookies (that last until you close your browser) or \"persistent\" cookies (that last until you or your browser delete them). For example, we may use cookies to store your language preferences or other OrcPub settings so you don‘t have to set them up every time you visit OrcPub. Some of the cookies we use are associated with your OrcPub account (including personal information about you, such as the email address you gave us), and other cookies are not." - "Device information. In addition to log data, we may also collect information about the device you're using OrcPub on, including what type of device it is, what operating system you're using, device settings, unique device identifiers, and crash data. Whether we collect some or all of this information often depends on what type of device you're using and its settings. For example, different types of information are available depending on whether you're using a Mac or a PC, or an iPhone or an Android phone. To learn more about what information your device makes available to us, please also check the policies of your device manufacturer or software provider."]} - {:title "Our partners and advertisers may share information with us" - :font-size 28 - :paragraphs - ["We may get information about you and your activity off OrcPub from our affiliates, advertisers, partners and other third parties we work with. For example:" - "Online advertisers typically share information with the websites or apps where they run ads to measure and/or improve those ads. We also receive this information, which may include information like whether clicks on ads led to purchases or a list of criteria to use in targeting ads."]}]} - {:title "How do we use the information we collect?" - :font-size 32 - :paragraphs - ["We use the information we collect to provide our products to you and make them better, develop new products, and protect OrcPub and our users. For example, we may log how often people use two different versions of a product, which can help us understand which version is better. If you make a purchase on OrcPub, we'll save your payment information and contact information so that you can use them the next time you want to buy something on OrcPub." - "We also use the information we collect to offer you customized content, including:" - "Showing you ads you might be interested in." - "We also use the information we collect to:" - "Send you updates (such as when certain activity, like shares or comments, happens on OrcPub), newsletters, marketing materials and other information that may be of interest to you. For example, depending on your email notification settings, we may send you weekly updates that include content you may like. You can decide to stop getting these updates by updating your account settings (or through other settings we may provide)." - "Help your friends and contacts find you on OrcPub. For example, if you sign up using a Facebook account, we may help your Facebook friends find your account on OrcPub when they first sign up for OrcPub. Or, we may allow people to search for your account on OrcPub using your email address." - "Respond to your questions or comments."]} - {:title "Transferring your Information" - :font-size 32 - :paragraphs - ["OrcPub is headquartered in the United States. By using our products or services, you authorize us to transfer and store your information inside the United States, for the purposes described in this policy. The privacy protections and the rights of authorities to access your personal information in such countries may not be equivalent to those in your home country."]} - {:title "How and when do we share information" - :font-size 32 - :paragraphs - ["Anyone can see the public role-playing game characters and other content you create, and the profile information you give us. We may also make this public information available through what are called \"APIs\" (basically a technical way to share information quickly). For example, a partner might use a OrcPub API to integrate with other applications our users may be interested in. The other limited instances where we may share your personal information include:" - "When we have your consent. This includes sharing information with other services (like Facebook or Twitter) when you've chosen to link to your OrcPub account to those services or publish your activity on OrcPub to them. For example, you can choose to share your characters on Facebook or Twitter." - "When you buy something on OrcPub using your credit card, we may share your credit card information, contact information, and other information about the transaction with the merchant you're buying from. The merchants treat this information just as if you had made a purchase from their website directly, which means their privacy policies and marketing policies apply to the information we share with them." - "Online advertisers typically use third party companies to audit the delivery and performance of their ads on websites and apps. We also allow these companies to collect this information on OrcPub. To learn more, please see our Help Center." - "We may employ third party companies or individuals to process personal information on our behalf based on our instructions and in compliance with this Privacy Policy. For example, we share credit card information with the payment companies we use to store your payment information. Or, we may share data with a security consultant to help us get better at identifying spam. In addition, some of the information we request may be collected by third party providers on our behalf." - "If we believe that disclosure is reasonably necessary to comply with a law, regulation or legal request; to protect the safety, rights, or property of the public, any person, or OrcPub; or to detect, prevent, or otherwise address fraud, security or technical issues. We may share the information described in this Policy with our wholly-owned subsidiaries and affiliates. We may engage in a merger, acquisition, bankruptcy, dissolution, reorganization, or similar transaction or proceeding that involves the transfer of the information described in this Policy. We may also share aggregated or non-personally identifiable information with our partners, advertisers or others."]} - {:title "What choices do you have about your information?" - :font-size 32 - :paragraphs - ["You may close your account at any time by emailing redorc@orcpub.com. We will then inactivate your account and remove your content from OrcPub. We may retain archived copies of you information as required by law or for legitimate business purposes (including to help address fraud and spam). You may remove any content you create from OrcPub at any time, although we may retain archived copies of the information. You may also disable sharing of content you create at any time, whether publicly shared or privately shared with specific users." - "Also, we support the Do Not Track browser setting."]} - {:title "Our policy on children's information" - :font-size 32 - :paragraphs - ["OrcPub is not directed to children under 13. If you learn that your minor child has provided us with personal information without your consent, please contact us."]} - {:title "How do we make changes to this policy?" - :font-size 32 - :paragraphs - ["We may change this policy from time to time, and if we do we'll post any changes on this page. If you continue to use OrcPub after those changes are in effect, you agree to the revised policy. If the changes are significant, we may provide more prominent notice or get your consent as required by law."]} - {:title "How can you contact us?" - :font-size 32 - :paragraphs - ["You can contact us by emailing redorc@orcpub.com"]}]}) + [{:title "Thank you for using OrcPub!" + :font-size 32 + :paragraphs + ["We wrote this policy to help you understand what information we collect, how we use it, and what choices you have. Because we're an internet company, some of the concepts below are a little technical, but we've tried our best to explain things in a simple and clear way. We welcome your questions and comments on this policy."]} + {:title "How We Collect Your Information" + :font-size 32 + :subsections + [{:title "When you give it to us or give us permission to obtain it" + :font-size 28 + :paragraphs + ["When you sign up for or use our products, you voluntarily give us certain information. This can include your name, profile photo, role-playing game characters, comments, likes, the email address or phone number you used to sign up, and any other information you provide us. If you're using OrcPub on your mobile device, you can also choose to provide us with location data. And if you choose to buy something on OrcPub, you provide us with payment information, contact information (ex., address and phone number), and what you purchased. If you buy something for someone else on OrcPub, you'd also provide us with their shipping details and contact information." + "You also may give us permission to access your information in other services. For example, you may link your Facebook or Twitter account to OrcPub, which allows us to obtain information from those accounts (like your friends or contacts). The information we get from those services often depends on your settings or their privacy policies, so be sure to check what those are."]} + {:title "We also get technical information when you use our products" + :font-size 28 + :paragraphs + ["These days, whenever you use a website, mobile application, or other internet service, there's certain information that almost always gets created and recorded automatically. The same is true when you use our products. Here are some of the types of information we collect:" + "Log data. When you use OrcPub, our servers may automatically record information (\"log data\"), including information that your browser sends whenever you visit a website or your mobile app sends when you're using it. This log data may include your Internet Protocol address, the address of the web pages you visited that had OrcPub features, browser type and settings, the date and time of your request, how you used OrcPub, and cookie data." + "Cookie data. Depending on how you're accessing our products, we may use \"cookies\" (small text files sent by your computer each time you visit our website, unique to your OrcPub account or your browser) or similar technologies to record log data. When we use cookies, we may use \"session\" cookies (that last until you close your browser) or \"persistent\" cookies (that last until you or your browser delete them). For example, we may use cookies to store your language preferences or other OrcPub settings so you don‘t have to set them up every time you visit OrcPub. Some of the cookies we use are associated with your OrcPub account (including personal information about you, such as the email address you gave us), and other cookies are not." + "Device information. In addition to log data, we may also collect information about the device you're using OrcPub on, including what type of device it is, what operating system you're using, device settings, unique device identifiers, and crash data. Whether we collect some or all of this information often depends on what type of device you're using and its settings. For example, different types of information are available depending on whether you're using a Mac or a PC, or an iPhone or an Android phone. To learn more about what information your device makes available to us, please also check the policies of your device manufacturer or software provider."]} + {:title "Our partners and advertisers may share information with us" + :font-size 28 + :paragraphs + ["We may get information about you and your activity off OrcPub from our affiliates, advertisers, partners and other third parties we work with. For example:" + "Online advertisers typically share information with the websites or apps where they run ads to measure and/or improve those ads. We also receive this information, which may include information like whether clicks on ads led to purchases or a list of criteria to use in targeting ads."]}]} + {:title "How do we use the information we collect?" + :font-size 32 + :paragraphs + ["We use the information we collect to provide our products to you and make them better, develop new products, and protect OrcPub and our users. For example, we may log how often people use two different versions of a product, which can help us understand which version is better. If you make a purchase on OrcPub, we'll save your payment information and contact information so that you can use them the next time you want to buy something on OrcPub." + "We also use the information we collect to offer you customized content, including:" + "Showing you ads you might be interested in." + "We also use the information we collect to:" + "Send you updates (such as when certain activity, like shares or comments, happens on OrcPub), newsletters, marketing materials and other information that may be of interest to you. For example, depending on your email notification settings, we may send you weekly updates that include content you may like. You can decide to stop getting these updates by updating your account settings (or through other settings we may provide)." + "Help your friends and contacts find you on OrcPub. For example, if you sign up using a Facebook account, we may help your Facebook friends find your account on OrcPub when they first sign up for OrcPub. Or, we may allow people to search for your account on OrcPub using your email address." + "Respond to your questions or comments."]} + {:title "Transferring your Information" + :font-size 32 + :paragraphs + ["OrcPub is headquartered in the United States. By using our products or services, you authorize us to transfer and store your information inside the United States, for the purposes described in this policy. The privacy protections and the rights of authorities to access your personal information in such countries may not be equivalent to those in your home country."]} + {:title "How and when do we share information" + :font-size 32 + :paragraphs + ["Anyone can see the public role-playing game characters and other content you create, and the profile information you give us. We may also make this public information available through what are called \"APIs\" (basically a technical way to share information quickly). For example, a partner might use a OrcPub API to integrate with other applications our users may be interested in. The other limited instances where we may share your personal information include:" + "When we have your consent. This includes sharing information with other services (like Facebook or Twitter) when you've chosen to link to your OrcPub account to those services or publish your activity on OrcPub to them. For example, you can choose to share your characters on Facebook or Twitter." + "When you buy something on OrcPub using your credit card, we may share your credit card information, contact information, and other information about the transaction with the merchant you're buying from. The merchants treat this information just as if you had made a purchase from their website directly, which means their privacy policies and marketing policies apply to the information we share with them." + "Online advertisers typically use third party companies to audit the delivery and performance of their ads on websites and apps. We also allow these companies to collect this information on OrcPub. To learn more, please see our Help Center." + "We may employ third party companies or individuals to process personal information on our behalf based on our instructions and in compliance with this Privacy Policy. For example, we share credit card information with the payment companies we use to store your payment information. Or, we may share data with a security consultant to help us get better at identifying spam. In addition, some of the information we request may be collected by third party providers on our behalf." + "If we believe that disclosure is reasonably necessary to comply with a law, regulation or legal request; to protect the safety, rights, or property of the public, any person, or OrcPub; or to detect, prevent, or otherwise address fraud, security or technical issues. We may share the information described in this Policy with our wholly-owned subsidiaries and affiliates. We may engage in a merger, acquisition, bankruptcy, dissolution, reorganization, or similar transaction or proceeding that involves the transfer of the information described in this Policy. We may also share aggregated or non-personally identifiable information with our partners, advertisers or others."]} + {:title "What choices do you have about your information?" + :font-size 32 + :paragraphs + (if (not (s/blank? (environ/env :email-access-key))) + ["You may close your account at any time by emailing " (environ/env :email-access-key) "We will then inactivate your account and remove your content from OrcPub. We may retain archived copies of you information as required by law or for legitimate business purposes (including to help address fraud and spam). "] + ["You may remove any content you create from OrcPub at any time, although we may retain archived copies of the information. You may also disable sharing of content you create at any time, whether publicly shared or privately shared with specific users." + "Also, we support the Do Not Track browser setting."])} + {:title "Our policy on children's information" + :font-size 32 + :paragraphs + ["OrcPub is not directed to children under 13. If you learn that your minor child has provided us with personal information without your consent, please contact us."]} + {:title "How do we make changes to this policy?" + :font-size 32 + :paragraphs + ["We may change this policy from time to time, and if we do we'll post any changes on this page. If you continue to use OrcPub after those changes are in effect, you agree to the revised policy. If the changes are significant, we may provide more prominent notice or get your consent as required by law."]} + (when (not (s/blank? (environ/env :email-access-key))) + {:title "How can you contact us?" + :font-size 32 + :paragraphs + ["You can contact us by emailing " (environ/env :email-access-key) ]})]}) (defn terms-page [sections] (page/html5 @@ -93,7 +98,7 @@ {:style "background-color:#2c3445"} [:div.content [:div.flex.justify-cont-s-b.align-items-c.w-100-p.p-l-20.p-r-20 - [:img.orcpub-logo.h-32.w-120.pointer {:src "/image/orcpub-logo.svg"}]]]] + [:img.h-60 {:src "/image/dmv-logo.svg"}]]]] [:div.container [:div.content [:div.f-s-24 @@ -202,7 +207,7 @@ {:title "Parties" :font-size 28 :paragraphs - ["These Terms are a contract between you and OrcPub, 2168 E Vimont Ave., Salt Lake City, UT 84109" + ["These Terms are a contract between you and OrcPub" "Effective May 1, 2017"]}]}]}) (defn terms-of-use [] diff --git a/src/clj/orcpub/routes.clj b/src/clj/orcpub/routes.clj index 1f577787b..49929d642 100644 --- a/src/clj/orcpub/routes.clj +++ b/src/clj/orcpub/routes.clj @@ -1,12 +1,10 @@ (ns orcpub.routes - (:require [io.pedestal.http :as http] + (:require [io.pedestal.http :as http] [io.pedestal.http.route :as route] [io.pedestal.test :as test] [io.pedestal.http.ring-middlewares :as ring] - [ring.middleware.cookies :only [wrap-cookies]] [ring.middleware.resource :as ring-resource] [ring.util.response :as ring-resp] - [ring.middleware.etag :refer [wrap-etag]] [io.pedestal.http.body-params :as body-params] [io.pedestal.interceptor.error :as error-int] [io.pedestal.interceptor.chain :refer [terminate]] @@ -16,12 +14,13 @@ [buddy.sign.jwt :as jwt] [buddy.hashers :as hashers] [buddy.auth.middleware :refer [authentication-request]] - [pandect.algo.sha1 :refer [sha1]] + [clojure.edn :as edn] [clojure.java.io :as io] [clj-time.core :as t :refer [hours from-now ago]] - [clj-time.coerce :as tc :refer [from-date]] + [clj-time.coerce :as tc] [clojure.string :as s] [clojure.spec.alpha :as spec] + [clojure.pprint] [orcpub.dnd.e5.skills :as skill5e] [orcpub.dnd.e5.character :as char5e] [orcpub.dnd.e5.spells :as spells] @@ -41,19 +40,16 @@ [orcpub.entity :as entity] [orcpub.security :as security] [orcpub.routes.party :as party] + [orcpub.routes.folder :as folder] [orcpub.oauth :as oauth] [hiccup.page :as page] [environ.core :as environ] - [clojure.set :as sets]) - (:import (org.apache.pdfbox.pdmodel.interactive.form PDCheckBox PDComboBox PDListBox PDRadioButton PDTextField) - - (org.apache.pdfbox.pdmodel PDDocument PDPage PDPageContentStream) - (org.apache.pdfbox.pdmodel.graphics.image PDImageXObject) - (java.io ByteArrayOutputStream ByteArrayInputStream) - (org.apache.pdfbox.pdmodel.graphics.image JPEGFactory LosslessFactory) - (org.eclipse.jetty.server.handler.gzip GzipHandler) - (javax.imageio ImageIO) - (java.net URL)) + [clojure.set :as sets] + [ring.middleware.head :as head] + [ring.util.codec :as codec] + [ring.util.request :as req]) + (:import (org.apache.pdfbox.pdmodel PDDocument PDPage PDPageContentStream) + (java.io ByteArrayOutputStream ByteArrayInputStream)) (:gen-class)) (deftype FixedBuffer [^long len]) @@ -72,10 +68,13 @@ :in $ ?username :where [?e :orcpub.user/username ?username]]) +;; Case-insensitive email lookup to guard against mixed-case legacy data. +;; Callers must pass a lowercased email. (def email-query '[:find ?e :in $ ?email - :where [?e :orcpub.user/email ?email]]) + :where [?e :orcpub.user/email ?stored] + [(clojure.string/lower-case ?stored) ?email]]) (defn find-user-by-username-or-email [db username-or-email] (d/q @@ -166,12 +165,28 @@ context (terminate-request context 401 "You don't own this party"))))}) +(defn folder-owner [db id] + (d/q '[:find ?owner . + :in $ ?id + :where [?id :orcpub.dnd.e5.folder/owner ?owner]] + db + id)) + +(def check-folder-owner + {:name :check-folder-owner + :enter (fn [context] + (let [{:keys [identity db] {:keys [id]} :path-params} (:request context) + folder-owner (folder-owner db id)] + (if (= (:user identity) folder-owner) + context + (terminate-request context 401 "You don't own this folder"))))}) + (defn redirect [route-key] (ring-resp/redirect (route-map/path-for route-key))) (defn verification-expired? [verification-sent] - (t/before? (from-date verification-sent) (-> 24 hours ago))) + (t/before? (tc/from-date verification-sent) (-> 24 hours ago))) (defn login-error [error-key & [data]] {:status 401 :body (merge @@ -188,14 +203,16 @@ (d/pull-many db '[:orcpub.user/username] ids))) (defn user-body [db user] - {:username (:orcpub.user/username user) - :email (:orcpub.user/email user) - :following (following-usernames db (map :db/id (:orcpub.user/following user)))}) + (cond-> {:username (:orcpub.user/username user) + :email (:orcpub.user/email user) + :following (following-usernames db (map :db/id (:orcpub.user/following user)))} + (:orcpub.user/pending-email user) + (assoc :pending-email (:orcpub.user/pending-email user)))) (defn bad-credentials-response [db username ip] (security/add-failed-login-attempt! username ip) (if (security/too-many-attempts-for-username? username) - (login-error errors/too-many-attempts) + (login-error errors/too-many-attempts) (let [user-for-username (find-user-by-username-or-email db username)] (login-error (if (:db/id user-for-username) errors/bad-credentials @@ -233,7 +250,7 @@ (try (let [resp (login-response request)] resp) - (catch Throwable e (do (prn "E" e) (throw e))))) + (catch Throwable e (prn "E" e) (throw e)))) (defn user-for-email [db email] @@ -246,46 +263,8 @@ (s/lower-case email))] user)) - -(defn get-or-create-oauth-user [conn db oauth-email] - (let [{:keys [:orcpub.user/username] :as user} (user-for-email db oauth-email)] - (if username - user - (let [result @(d/transact - conn - [{:orcpub.user/email oauth-email - :orcpub.user/username oauth-email - :orcpub.user/send-updates? false - :orcpub.user/created (java.util.Date.) - :orcpub.user/verified? true}])] - (user-for-email (d/db conn) oauth-email))))) - -(defn oauth-login [email-fn] - (fn [{:keys [conn db] :as request}] - (let [fb-email (email-fn request) - user (get-or-create-oauth-user conn db fb-email)] - (create-login-response db user)))) - -(defn fb-login [{:keys [json-params db conn remote-addr] :as request}] - (if-let [access-token (-> json-params :authResponse :accessToken)] - (let [fb-user (oauth/get-fb-user access-token)] - (if-let [email (:email fb-user)] - (create-login-response db (get-or-create-oauth-user conn db email)) - (login-error errors/fb-email-permission))) - {:status 400})) - -(def google-login - (oauth-login oauth/get-google-email)) - - -(defn google-oauth-code [request] - (ring-resp/redirect (str oauth/google-oauth-url (oauth/get-google-redirect-uri request)))) - -(defn fb-oauth-code [request] - (ring-resp/redirect (str oauth/fb-oauth-url (oauth/get-fb-redirect-uri request)))) - (defn base-url [{:keys [scheme headers]}] - (str (name scheme) "://" (headers "host"))) + (str (or (headers "x-forwarded-proto") (name scheme)) "://" (headers "host"))) (defn send-verification-email [request params verification-key] (email/send-verification-email @@ -293,24 +272,36 @@ params verification-key)) +(defn send-email-change-verification [request params verification-key] + (email/send-email-change-verification + (base-url request) + params + verification-key)) + (defn do-verification [request params conn & [tx-data]] (let [verification-key (str (java.util.UUID/randomUUID)) now (java.util.Date.)] - (do @(d/transact - conn - [(merge - tx-data - {:orcpub.user/verified? false - :orcpub.user/verification-key verification-key - :orcpub.user/verification-sent now})]) - (send-verification-email request params verification-key) - {:status 200}))) + (try + @(d/transact + conn + [(merge + tx-data + {:orcpub.user/verified? false + :orcpub.user/verification-key verification-key + :orcpub.user/verification-sent now})]) + (send-verification-email request params verification-key) + {:status 200} + (catch Exception e + (println "ERROR: Failed to create verification record:" (.getMessage e)) + (throw (ex-info "Unable to complete registration. Please try again or contact support." + {:error :verification-failed} + e)))))) (defn register [{:keys [json-params db conn] :as request}] (let [{:keys [username email password send-updates?]} json-params - username (if username (s/trim username)) - email (if email (s/lower-case (s/trim email))) - password (if password (s/trim password)) + username (when username (s/trim username)) + email (when email (s/lower-case (s/trim email))) + password (when password (s/trim password)) validation (registration/validate-registration json-params (seq (d/q email-query db email)) @@ -328,7 +319,7 @@ :orcpub.user/password (hashers/encrypt password) :orcpub.user/send-updates? send-updates? :orcpub.user/created (java.util.Date.)})) - (catch Throwable e (do (prn e) (throw e)))))) + (catch Throwable e (prn e) (throw e))))) (def user-for-verification-key-query '[:find ?e @@ -356,16 +347,43 @@ (let [{:keys [:orcpub.user/verification-sent :orcpub.user/verified? :orcpub.user/username + :orcpub.user/pending-email :db/id] :as user} (user-for-verification-key (d/db conn) key)] (if username - (if verified? + (cond + (and verified? (nil? pending-email)) (redirect route-map/verify-success-route) - (if (or (nil? verification-sent) - (verification-expired? verification-sent)) - (redirect route-map/verify-failed-route) - (do (d/transact conn [{:db/id id - :orcpub.user/verified? true}]) - (redirect route-map/verify-success-route)))) + + (or (nil? verification-sent) + (verification-expired? verification-sent)) + ;; Clean up stale pending state so user can request a fresh change + (do (let [retractions (cond-> [[:db/retract id :orcpub.user/verification-key key] + [:db/retract id :orcpub.user/verification-sent verification-sent]] + pending-email + (conj [:db/retract id :orcpub.user/pending-email pending-email]))] + @(d/transact conn retractions)) + (redirect route-map/verify-failed-route)) + + pending-email + ;; Guard: re-check that the target email hasn't been claimed since request. + ;; All paths retract verification-key and verification-sent to prevent + ;; link reuse and avoid stale rate-limit data. + (if (seq (d/q email-query (d/db conn) pending-email)) + (do @(d/transact conn [[:db/retract id :orcpub.user/pending-email pending-email] + [:db/retract id :orcpub.user/verification-key key] + [:db/retract id :orcpub.user/verification-sent verification-sent]]) + (redirect route-map/verify-failed-route)) + (do @(d/transact conn [{:db/id id + :orcpub.user/email pending-email} + [:db/retract id :orcpub.user/pending-email pending-email] + [:db/retract id :orcpub.user/verification-key key] + [:db/retract id :orcpub.user/verification-sent verification-sent]]) + (redirect route-map/verify-success-route))) + + :else + (do @(d/transact conn [{:db/id id + :orcpub.user/verified? true}]) + (redirect route-map/verify-success-route))) {:status 400})) {:status 400})) @@ -378,23 +396,30 @@ (redirect route-map/verify-success-route) (do-verification request (merge query-params - {:first-and-last-name "OrcPub Patron"}) + {:first-and-last-name "DMV Patron"}) conn {:db/id id})))) (defn do-send-password-reset [user-id email conn request] (let [key (str (java.util.UUID/randomUUID))] - @(d/transact - conn - [{:db/id user-id - :orcpub.user/password-reset-key key - :orcpub.user/password-reset-sent (java.util.Date.)}]) - (email/send-reset-email - (base-url request) - {:first-and-last-name "OrcPub Patron" - :email email} - key) - {:status 200})) + (try + @(d/transact + conn + [{:db/id user-id + :orcpub.user/password-reset-key key + :orcpub.user/password-reset-sent (java.util.Date.)}]) + (email/send-reset-email + (base-url request) + {:first-and-last-name "DMV Patron" + :email email} + key) + {:status 200} + (catch Exception e + (println "ERROR: Failed to initiate password reset for user" user-id ":" (.getMessage e)) + (throw (ex-info "Unable to initiate password reset. Please try again or contact support." + {:error :password-reset-failed + :user-id user-id} + e)))))) (defn password-reset-expired? [password-reset-sent] (and password-reset-sent (t/before? (tc/from-date password-reset-sent) (-> 24 hours ago)))) @@ -413,15 +438,23 @@ (if id (do-send-password-reset id email conn request) {:status 400 :body {:error :no-account}})) - (catch Throwable e (do (prn e) (throw e))))) + (catch Throwable e (prn e) (throw e)))) (defn do-password-reset [conn user-id password] - @(d/transact - conn - [{:db/id user-id - :orcpub.user/password (hashers/encrypt (s/trim password)) - :orcpub.user/password-reset (java.util.Date.)}]) - {:status 200}) + (try + @(d/transact + conn + [{:db/id user-id + :orcpub.user/password (hashers/encrypt (s/trim password)) + :orcpub.user/password-reset (java.util.Date.) + :orcpub.user/verified? true}]) + {:status 200} + (catch Exception e + (println "ERROR: Failed to reset password for user" user-id ":" (.getMessage e)) + (throw (ex-info "Unable to reset password. Please try again or contact support." + {:error :password-update-failed + :user-id user-id} + e))))) (defn reset-password [{:keys [json-params db conn cookies identity] :as request}] (try @@ -432,7 +465,7 @@ (not= password verify-password) {:status 400 :message "Passwords do not match"} (seq (registration/validate-password password)) {:status 400 :message "New password is invalid"} :else (do-password-reset conn id password))) - (catch Throwable t (do (prn t) (throw t))))) + (catch Throwable t (prn t) (throw t)))) (def font-sizes (merge @@ -452,7 +485,7 @@ :weapon-name-2 8 :weapon-name-3 8})) -(defn add-spell-cards! [doc spells-known spell-save-dcs spell-attack-mods custom-spells] (try +(defn add-spell-cards! [doc spells-known spell-save-dcs spell-attack-mods custom-spells print-spell-card-dc-mod?] (try (let [custom-spells-map (common/map-by-key custom-spells) spells-map (merge spells/spell-map custom-spells-map) flat-spells (-> spells-known vals flatten) @@ -463,7 +496,7 @@ class) key]) flat-spells) - parts (vec (partition-all 9 sorted-spells))] + parts (vec (partition-all 9 flat-spells))] (doseq [i (range (count parts)) :let [part (parts i)]] (let [page (PDPage.)] @@ -486,7 +519,8 @@ 2.5 3.5 spells - i)) + i + print-spell-card-dc-mod?)) back-page (PDPage.)] (with-open [back-page-cs (PDPageContentStream. doc back-page)] (.addPage doc back-page) @@ -494,34 +528,61 @@ (catch Exception e (prn "FAILED ADDING SPELLS CARDS!" e)))) (defn character-pdf-2 [req] - (let [fields (-> req :form-params :body clojure.edn/read-string) - {:keys [image-url image-url-failed faction-image-url faction-image-url-failed spells-known custom-spells spell-save-dcs spell-attack-mods print-spell-cards?]} fields + (let [fields (try + (-> req :form-params :body edn/read-string) + (catch Exception e + (throw (ex-info "Invalid character data format. Unable to parse PDF request." + {:error :invalid-pdf-data} + e)))) + + {:keys [image-url image-url-failed faction-image-url faction-image-url-failed spells-known custom-spells spell-save-dcs spell-attack-mods print-spell-cards? print-character-sheet-style? print-spell-card-dc-mod? character-name class-level player-name]} fields + + sheet6 (str "fillable-char-sheetstyle-" print-character-sheet-style? "-6-spells.pdf") + sheet5 (str "fillable-char-sheetstyle-" print-character-sheet-style? "-5-spells.pdf") + sheet4 (str "fillable-char-sheetstyle-" print-character-sheet-style? "-4-spells.pdf") + sheet3 (str "fillable-char-sheetstyle-" print-character-sheet-style? "-3-spells.pdf") + sheet2 (str "fillable-char-sheetstyle-" print-character-sheet-style? "-2-spells.pdf") + sheet1 (str "fillable-char-sheetstyle-" print-character-sheet-style? "-1-spells.pdf") + sheet0 (str "fillable-char-sheetstyle-" print-character-sheet-style? "-0-spells.pdf") input (.openStream (io/resource (cond - (find fields :spellcasting-class-6) "fillable-char-sheet-6-spells.pdf" - (find fields :spellcasting-class-5) "fillable-char-sheet-5-spells.pdf" - (find fields :spellcasting-class-4) "fillable-char-sheet-4-spells.pdf" - (find fields :spellcasting-class-3) "fillable-char-sheet-3-spells.pdf" - (find fields :spellcasting-class-2) "fillable-char-sheet-2-spells.pdf" - (find fields :spellcasting-class-1) "fillable-char-sheet-1-spells.pdf" - :else "fillable-char-sheet-0-spells.pdf"))) + (find fields :spellcasting-class-6) sheet6 + (find fields :spellcasting-class-5) sheet5 + (find fields :spellcasting-class-4) sheet4 + (find fields :spellcasting-class-3) sheet3 + (find fields :spellcasting-class-2) sheet2 + (find fields :spellcasting-class-1) sheet1 + :else sheet0))) output (ByteArrayOutputStream.) user-agent (get-in req [:headers "user-agent"]) - chrome? (re-matches #".*Chrome.*" user-agent)] + chrome? (re-matches #".*Chrome.*" user-agent) + filename (str player-name " - " character-name " - " class-level ".pdf")] + (with-open [doc (PDDocument/load input)] (pdf/write-fields! doc fields (not chrome?) font-sizes) - (if (and print-spell-cards? (seq spells-known)) - (add-spell-cards! doc spells-known spell-save-dcs spell-attack-mods custom-spells)) - (if (and image-url + (when (and print-spell-cards? (seq spells-known)) + (add-spell-cards! doc spells-known spell-save-dcs spell-attack-mods custom-spells print-spell-card-dc-mod?)) + + (when (and image-url (re-matches #"^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]" image-url) (not image-url-failed)) - (pdf/draw-image! doc (pdf/get-page doc 1) image-url 0.45 1.75 2.35 3.15)) - (if (and faction-image-url + (case print-character-sheet-style? + 1 (pdf/draw-image! doc (pdf/get-page doc 1) image-url 0.45 1.75 2.35 3.15) + 2 (pdf/draw-image! doc (pdf/get-page doc 1) image-url 0.45 1.75 2.35 3.15) + 3 (pdf/draw-image! doc (pdf/get-page doc 1) image-url 0.45 1.75 2.35 3.15) + 4 (pdf/draw-image! doc (pdf/get-page doc 0) image-url 0.50 0.85 2.35 3.15))) + (when (and faction-image-url (re-matches #"^(https?|ftp|file)://[-a-zA-Z0-9+&@#/%?=~_|!:,.;]*[-a-zA-Z0-9+&@#/%=~_|]" faction-image-url) (not faction-image-url-failed)) - (pdf/draw-image! doc (pdf/get-page doc 1) faction-image-url 5.88 2.4 1.905 1.52)) + (case print-character-sheet-style? + 1 (pdf/draw-image! doc (pdf/get-page doc 1) faction-image-url 5.88 2.4 1.905 1.52) + 2 (pdf/draw-image! doc (pdf/get-page doc 1) faction-image-url 5.88 2.4 1.905 1.52) + 3 (pdf/draw-image! doc (pdf/get-page doc 1) faction-image-url 5.88 2.0 1.905 1.52) + 4 ())) (.save doc output)) (let [a (.toByteArray output)] - {:status 200 :body (ByteArrayInputStream. a)}))) + {:status 200 + :headers {"Content-Disposition" (str "inline; filename=\"" filename "\"")} + :body (ByteArrayInputStream. a)}))) (defn html-response [html & [response]] @@ -532,24 +593,19 @@ :headers {"Content-Type" "text/html"}})] merged)) -(defn empty-index [req & [response]] - (html-response - (slurp (io/resource "public/blank.html")) - response)) - (def user-by-password-reset-key-query '[:find ?e :in $ ?key :where [?e :orcpub.user/password-reset-key ?key]]) (def default-title - "The New OrcPub: D&D 5e Character Builder/Generator") + "Dungeon Master's Vault: D&D 5e Character Builder/Generator") (def default-description "Dungeons & Dragons 5th Edition (D&D 5e) character builder/generator and digital character sheet far beyond any other in the multiverse.") (defn default-image-url [host] - (str "http://" host "/image/orcpub-box-logo.png")) + (str "http://" host "/image/dmv-box-logo.png")) (defn index-page-response [{:keys [headers uri] :as request} {:keys [title description image-url]} @@ -558,8 +614,7 @@ (merge response {:status 200 - :headers {"Content-Type" "text/html" - "Access-Control-Allow-Origin" "https://www.facebook.com"} + :headers {"Content-Type" "text/html" } :body (index-page {:url (str "http://" host uri) @@ -582,7 +637,7 @@ :orcpub.user/password-reset-sent :orcpub.user/password-reset] :as user} (first-user-by db user-by-password-reset-key-query key) - expired? (password-reset-expired? password-reset-sent) + expired? (password-reset-expired? password-reset-sent) already-reset? (password-already-reset? password-reset password-reset-sent)] (cond expired? (redirect route-map/password-reset-expired-route) @@ -603,7 +658,7 @@ (check-field username-query (:username query-params) db)) (defn check-email [{:keys [db query-params]}] - (check-field email-query (:email query-params) db)) + (check-field email-query (some-> (:email query-params) s/lower-case) db)) (defn character-for-id [db id] (d/pull db '[*] id)) @@ -619,14 +674,21 @@ (-> result :tempids (get temp-id))) (defn create-entity [conn username entity owner-prop] - (as-> entity $ - (entity/remove-ids $) - (assoc $ - :db/id "tempid" - owner-prop username) - @(d/transact conn [$]) - (get-new-id "tempid" $) - (d/pull (d/db conn) '[*] $))) + (try + (as-> entity $ + (entity/remove-ids $) + (assoc $ + :db/id "tempid" + owner-prop username) + @(d/transact conn [$]) + (get-new-id "tempid" $) + (d/pull (d/db conn) '[*] $)) + (catch Exception e + (println "ERROR: Failed to create entity for user" username ":" (.getMessage e)) + (throw (ex-info "Unable to create entity. Please try again or contact support." + {:error :entity-creation-failed + :username username} + e))))) (defn email-for-username [db username] (d/q '[:find ?email . @@ -638,25 +700,35 @@ username)) (defn update-entity [conn username entity owner-prop] - (let [id (:db/id entity) - current (d/pull (d/db conn) '[*] id) - owner (get current owner-prop) - email (email-for-username (d/db conn) username)] - (if ((set [username email]) owner) - (let [current-ids (entity/db-ids current) - new-ids (entity/db-ids entity) - retract-ids (sets/difference current-ids new-ids) - retractions (map - (fn [retract-id] - [:db/retractEntity retract-id]) - retract-ids) - remove-ids (sets/difference new-ids current-ids) - with-ids-removed (entity/remove-specific-ids entity remove-ids) - new-entity (assoc with-ids-removed owner-prop username) - result @(d/transact conn (concat retractions [new-entity]))] - (d/pull (d/db conn) '[*] id)) - (throw (ex-info "Not user entity" - {:error :not-user-entity}))))) + (try + (let [id (:db/id entity) + current (d/pull (d/db conn) '[*] id) + owner (get current owner-prop) + email (email-for-username (d/db conn) username)] + (if ((set [username email]) owner) + (let [current-ids (entity/db-ids current) + new-ids (entity/db-ids entity) + retract-ids (sets/difference current-ids new-ids) + retractions (map + (fn [retract-id] + [:db/retractEntity retract-id]) + retract-ids) + remove-ids (sets/difference new-ids current-ids) + with-ids-removed (entity/remove-specific-ids entity remove-ids) + new-entity (assoc with-ids-removed owner-prop username) + result @(d/transact conn (concat retractions [new-entity]))] + (d/pull (d/db conn) '[*] id)) + (throw (ex-info "Not user entity" + {:error :not-user-entity})))) + (catch clojure.lang.ExceptionInfo e + (throw e)) + (catch Exception e + (println "ERROR: Failed to update entity for user" username ":" (.getMessage e)) + (throw (ex-info "Unable to update entity. Please try again or contact support." + {:error :entity-update-failed + :username username + :entity-id (:db/id entity)} + e))))) (defn save-entity [conn username e owner-prop] (let [without-empty-fields (entity/remove-empty-fields e)] @@ -696,12 +768,13 @@ (let [current-character (d/pull db '[*] id) problems [] #_(dnd-e5-char-type-problems current-character) current-valid? (spec/valid? ::se/entity current-character)] - (if (not current-valid?) - (do (prn "INVALID CHARACTER FOUND, REPLACING" #_current-character) - (prn "INVALID CHARACTER EXPLANATION" #_(spec/explain-data ::se/entity current-character)))) + (when-not current-valid? + (prn "INVALID CHARACTER FOUND, REPLACING" #_current-character) + (prn "INVALID CHARACTER EXPLANATION" #_(spec/explain-data ::se/entity current-character))) (if (seq problems) - {:status 400 :body problems} - (if (not current-valid?) + (throw (ex-info "Character has problems" + {:error :character-problems :problems problems})) + (if-not current-valid? (let [new-character (entity/remove-ids character) tx [[:db/retractEntity (:db/id current-character)] (-> new-character @@ -709,8 +782,7 @@ :orcpub.entity.strict/owner username) add-dnd-5e-character-tags)] result @(d/transact conn tx)] - {:status 200 - :body (d/pull (d/db conn) '[*] (-> result :tempids (get "tempid")))}) + (d/pull (d/db conn) '[*] (-> result :tempids (get "tempid")))) (let [new-character (entity/remove-orphan-ids character) current-ids (entity/db-ids current-character) new-ids (entity/db-ids new-character) @@ -724,26 +796,41 @@ (assoc :orcpub.entity.strict/owner username) add-dnd-5e-character-tags))] @(d/transact conn tx) - {:status 200 - :body (d/pull (d/db conn) '[*] id)})))) - {:status 401 :body "You do not own this character"}))) - -(defn create-new-character [conn character username] - (let [result @(d/transact conn - [(-> character - (assoc :db/id "tempid" - ::se/owner username) - add-dnd-5e-character-tags)]) - new-id (get-new-id "tempid" result)] - {:status 200 - :body (d/pull (d/db conn) '[*] new-id)})) + (d/pull (d/db conn) '[*] id))))) + (throw (ex-info "Not user character" + {:error :not-user-character}))))) + +(defn create-new-character + "Creates a new D&D 5e character. + + Args: + conn - Database connection + character - Character data map + username - Owner username + + Returns: + Created character entity + + Throws: + ExceptionInfo on database failure" + [conn character username] + (errors/with-db-error-handling :character-creation-failed + {:username username} + "Unable to create character. Please try again or contact support." + (let [result @(d/transact conn + [(-> character + (assoc :db/id "tempid" + ::se/owner username) + add-dnd-5e-character-tags)]) + new-id (get-new-id "tempid" result)] + (d/pull (d/db conn) '[*] new-id)))) (defn clean-up-character [character] (if (-> character ::se/values ::char5e/xps string?) (update-in character [::se/values ::char5e/xps] #(try - (if (not (s/blank? %)) + (if-not (s/blank? %) (Long/parseLong %) 0) (catch NumberFormatException e 0))) @@ -756,11 +843,17 @@ (try (if-let [data (spec/explain-data ::se/entity character)] {:status 400 :body data} - (let [clean-character (clean-up-character character)] - (if (:db/id clean-character) - (update-character db conn clean-character username) - (create-new-character conn clean-character username)))) - (catch Exception e (do (prn "ERROR" e) (throw e)))))) + (let [clean-character (clean-up-character character) + updated-character (if (:db/id clean-character) + (update-character db conn clean-character username) + (create-new-character conn clean-character username))] + {:status 200 :body updated-character})) + (catch clojure.lang.ExceptionInfo e + (let [data (ex-data e)] + (case (:error data) + :character-problems {:status 400 :body (:problems data)} + :not-user-character {:status 401 :body "You do not own this character"}))) + (catch Exception e (prn "ERROR" e) (throw e))))) (defn save-character [{:keys [db transit-params body conn identity] :as request}] (do-save-character db conn transit-params identity)) @@ -784,10 +877,23 @@ :body item} {:status 404}))) -(defn delete-item [{:keys [db conn username] {:keys [:id]} :path-params}] +(defn delete-item + "Deletes a magic item owned by the user. + + Args: + request - HTTP request with item ID + + Returns: + HTTP 200 on success, 401 if not owned + + Throws: + ExceptionInfo on database failure" + [{:keys [db conn username] {:keys [:id]} :path-params}] (let [{:keys [::mi5e/owner]} (d/pull db '[::mi5e/owner] id)] (if (= username owner) - (do + (errors/with-db-error-handling :item-deletion-failed + {:item-id id} + "Unable to delete item. Please try again or contact support." @(d/transact conn [[:db/retractEntity id]]) {:status 200}) {:status 401}))) @@ -842,29 +948,73 @@ results)] {:status 200 :body characters})) -(defn follow-user [{:keys [db conn identity] {:keys [user]} :path-params}] +(defn follow-user + "Adds a user to the authenticated user's following list. + + Args: + request - HTTP request with username to follow + + Returns: + HTTP 200 on success + + Throws: + ExceptionInfo on database failure" + [{:keys [db conn identity] {:keys [user]} :path-params}] (let [other-user-id (user-id-for-username db user) username (:user identity) user-id (user-id-for-username db username)] - @(d/transact conn [{:db/id user-id - :orcpub.user/following other-user-id}]) - {:status 200})) + (errors/with-db-error-handling :follow-user-failed + {:follower username :followed user} + "Unable to follow user. Please try again or contact support." + @(d/transact conn [{:db/id user-id + :orcpub.user/following other-user-id}]) + {:status 200}))) + +(defn unfollow-user + "Removes a user from the authenticated user's following list. + + Args: + request - HTTP request with username to unfollow + + Returns: + HTTP 200 on success -(defn unfollow-user [{:keys [db conn identity] {:keys [user]} :path-params}] + Throws: + ExceptionInfo on database failure" + [{:keys [db conn identity] {:keys [user]} :path-params}] (let [other-user-id (user-id-for-username db user) username (:user identity) user-id (user-id-for-username db username)] - @(d/transact conn [[:db/retract user-id :orcpub.user/following other-user-id]]) - {:status 200})) - -(defn delete-character [{:keys [db conn identity] {:keys [id]} :path-params}] - (let [parsed-id (Long/parseLong id) + (errors/with-db-error-handling :unfollow-user-failed + {:follower username :unfollowed user} + "Unable to unfollow user. Please try again or contact support." + @(d/transact conn [[:db/retract user-id :orcpub.user/following other-user-id]]) + {:status 200}))) + +(defn delete-character + "Deletes a character owned by the authenticated user. + + Args: + request - HTTP request with character ID in path params + + Returns: + HTTP 200 on success, 400 for problems, 401 if not owned + + Throws: + ExceptionInfo on invalid ID or database failure" + [{:keys [db conn identity] {:keys [id]} :path-params}] + (let [parsed-id (errors/with-validation :invalid-character-id + {:id id} + "Invalid character ID format" + (Long/parseLong id)) username (:user identity) character (d/pull db '[*] parsed-id) problems [] #_(dnd-e5-char-type-problems character)] (if (owns-entity? db username parsed-id) (if (empty? problems) - (do + (errors/with-db-error-handling :character-deletion-failed + {:character-id parsed-id} + "Unable to delete character. Please try again or contact support." @(d/transact conn [[:db/retractEntity parsed-id]]) {:status 200}) {:status 400 :body problems}) @@ -878,10 +1028,26 @@ {:status 200 :body character}))) (defn character-summary-for-id [db id] - {:keys [::se/summary]} (d/pull db '[::se/summary {::se/values [::char5e/description ::char5e/image-url]}] id)) - -(defn get-character [{:keys [db] {:keys [:id]} :path-params}] - (let [parsed-id (Long/parseLong id)] + ;; Fixed: bare destructuring outside let silently returned nil + (let [{:keys [::se/summary]} (d/pull db '[::se/summary {::se/values [::char5e/description ::char5e/image-url]}] id)] + summary)) + +(defn get-character + "Retrieves a character by ID. + + Args: + request - HTTP request with character ID in path params + + Returns: + HTTP response with character data + + Throws: + ExceptionInfo on invalid ID format" + [{:keys [db] {:keys [:id]} :path-params}] + (let [parsed-id (errors/with-validation :invalid-character-id + {:id id} + "Invalid character ID format" + (Long/parseLong id))] (get-character-for-id db parsed-id))) (defn get-user [{:keys [db identity]}] @@ -889,22 +1055,136 @@ user (find-user-by-username-or-email db username)] {:status 200 :body (user-body db user)})) -(defn delete-user [{:keys [db conn identity]}] +(defn delete-user + "Deletes the authenticated user's account. + + Args: + request - HTTP request with authenticated user identity + + Returns: + HTTP 200 on success + + Throws: + ExceptionInfo on database failure" + [{:keys [db conn identity]}] (let [username (:user identity) user (d/q '[:find ?u . :in $ ?username :where [?u :orcpub.user/username ?username]] db username)] - @(d/transact conn [[:db/retractEntity user]]) - {:status 200})) + (errors/with-db-error-handling :user-deletion-failed + {:username username} + "Unable to delete user account. Please try again or contact support." + @(d/transact conn [[:db/retractEntity user]]) + {:status 200}))) + +(defn rate-limit-remaining-secs + "Seconds until the user can act again. In the 0–1 min zone (email in transit) + returns time until the 1-min resend window opens. In the 1–5 min zone (for a + different email) returns time until the 5-min cooldown expires." + [verification-sent new-email pending-email] + (when verification-sent + (let [elapsed-ms (- (System/currentTimeMillis) (.getTime ^java.util.Date verification-sent)) + ;; If same email, they're waiting for the 1-min resend window to open. + ;; If different email, they're waiting for the full 5-min cooldown. + target-ms (if (= new-email pending-email) + (* 1 60 1000) + (* 5 60 1000)) + remaining-ms (- target-ms elapsed-ms)] + (when (pos? remaining-ms) + (int (Math/ceil (/ remaining-ms 1000.0))))))) + +(defn email-change-rate-limited? [verification-sent pending-email new-email] + ;; Only rate-limit if the last key was generated for a pending email change + ;; (not for initial registration verification). + ;; Three zones from verification-sent: + ;; 0–1 min → too soon, email is in transit (always blocked) + ;; 1–5 min → free resend allowed for same email, otherwise blocked + ;; 5+ min → open for any request + (and pending-email + verification-sent + (let [elapsed-ms (- (System/currentTimeMillis) (.getTime ^java.util.Date verification-sent)) + same-email? (= new-email pending-email)] + (cond + (>= elapsed-ms (* 5 60 1000)) false ;; past cooldown + (< elapsed-ms (* 1 60 1000)) true ;; too soon + :else (not same-email?)))) ;; 1-5 min: resend ok, new email blocked + ) + +(defn request-email-change [{:keys [transit-params db conn identity] :as request}] + (try + ;; Client sends {:new-email "..."} (confirm-email is validated client-side only) + (let [new-email (s/lower-case (s/trim (str (:new-email transit-params)))) + username (:user identity)] + (if (nil? username) + {:status 400 :body {:error :user-not-found}} + (let [{:keys [:db/id + :orcpub.user/email + :orcpub.user/pending-email + :orcpub.user/verification-sent] :as user} (find-user-by-username db username)] + (cond + (nil? id) + {:status 400 :body {:error :user-not-found}} + + (registration/bad-email? new-email) + {:status 400 :body {:error :invalid-email}} + + (= new-email (some-> email s/lower-case)) + {:status 400 :body {:error :same-as-current}} + + (email-change-rate-limited? verification-sent pending-email new-email) + {:status 429 :body {:error :too-many-requests + :retry-after-secs (rate-limit-remaining-secs verification-sent new-email pending-email)}} + + ;; Check no other account already owns this email + (seq (d/q email-query db new-email)) + {:status 400 :body {:error :email-taken}} + + ;; Free resend: same email, 1–5 min after original send. Re-send with + ;; existing key and don't update verification-sent (no rolling window). + (and (= new-email pending-email) + verification-sent + (let [elapsed (- (System/currentTimeMillis) (.getTime ^java.util.Date verification-sent))] + (and (>= elapsed (* 1 60 1000)) + (< elapsed (* 5 60 1000))))) + (try + (send-email-change-verification request + {:email new-email :username username} + (:orcpub.user/verification-key user)) + {:status 200 :body {:pending-email new-email}} + (catch Throwable e + (prn "Email resend failed:" (.getMessage e)) + {:status 500 :body {:error :email-send-failed}})) + + :else + (let [verification-key (str (java.util.UUID/randomUUID)) + now (java.util.Date.)] + @(d/transact conn [{:db/id id + :orcpub.user/pending-email new-email + :orcpub.user/verification-key verification-key + :orcpub.user/verification-sent now}]) + ;; Roll back pending-email if verification email fails to send + (try + (send-email-change-verification request + {:email new-email :username username} + verification-key) + {:status 200 :body {:pending-email new-email}} + (catch Throwable e + (prn "Email send failed, rolling back pending state:" (.getMessage e)) + ;; Full rollback: retract all attributes set by the failed attempt + @(d/transact conn [[:db/retract id :orcpub.user/pending-email new-email] + [:db/retract id :orcpub.user/verification-key verification-key] + [:db/retract id :orcpub.user/verification-sent now]]) + {:status 500 :body {:error :email-send-failed}}))))))) + (catch Throwable e (prn e) (throw e)))) (defn character-summary-description [{:keys [::char5e/race-name ::char5e/subrace-name ::char5e/classes]}] (str race-name " " - (if subrace-name (str "(" subrace-name ") ")) + (when subrace-name (str "(" subrace-name ") ")) " " - (if (seq classes) + (when (seq classes) (s/join " / " (map @@ -933,6 +1213,7 @@ [route-map/dnd-e5-class-builder-page-route] [route-map/dnd-e5-language-builder-page-route] [route-map/dnd-e5-invocation-builder-page-route] + [route-map/dnd-e5-boon-builder-page-route] [route-map/dnd-e5-feat-builder-page-route] [route-map/dnd-e5-item-list-page-route] [route-map/dnd-e5-item-page-route :key ":key"] @@ -998,8 +1279,7 @@ (def expanded-index-routes (route/expand-routes - (into #{} index-page-routes))) - + (set index-page-routes))) (def service-error-handler (error-int/error-dispatch [ctx ex] @@ -1016,12 +1296,21 @@ (def get-js get-file) -(def get-fa get-file) - (def get-image get-file) (def get-favicon get-file) +(def webjars-root "META-INF/resources/webjars/") + +(defn get-webjar + "Get a resource containd within a webjar. + Expects route to be /assets/*" + [request] + (let [path (subs (codec/url-decode (req/path-info request)) 1) + new-path (s/replace-first path #"^assets/" webjars-root)] + (-> (ring-resp/resource-response new-path) + (head/head-response request)))) + (def routes (concat (route/expand-routes @@ -1029,14 +1318,16 @@ ^:interceptors [(body-params/body-params) service-error-handler] ["/js/*" {:get `get-js}] ["/css/*" {:get `get-css}] - ["/font-awesome-4.7.0/*" {:get `get-fa}] + ["/assets/*" {:get `get-webjar}] ["/image/*" {:get `get-image}] - ["/favicon.ico" {:get `get-favicon}] + ["/favicon/*" {:get `get-favicon}] [(route-map/path-for route-map/register-route) {:post `register}] [(route-map/path-for route-map/user-route) ^:interceptors [check-auth] {:get `get-user :delete `delete-user}] + [(route-map/path-for route-map/user-email-route) ^:interceptors [check-auth] + {:put `request-email-change}] [(route-map/path-for route-map/follow-user-route :user ":user") ^:interceptors [check-auth] {:post `follow-user :delete `unfollow-user}] @@ -1074,16 +1365,19 @@ {:post `party/add-character}] [(route-map/path-for route-map/dnd-e5-char-party-character-route :id ":id" :character-id ":character-id") ^:interceptors [check-auth parse-id check-party-owner] {:delete `party/remove-character}] + [(route-map/path-for route-map/dnd-e5-char-folders-route) ^:interceptors [check-auth] + {:post `folder/create-folder + :get `folder/folders}] + [(route-map/path-for route-map/dnd-e5-char-folder-route :id ":id") ^:interceptors [check-auth parse-id check-folder-owner] + {:delete `folder/delete-folder}] + [(route-map/path-for route-map/dnd-e5-char-folder-name-route :id ":id") ^:interceptors [check-auth parse-id check-folder-owner] + {:put `folder/update-folder-name}] + [(route-map/path-for route-map/dnd-e5-char-folder-characters-route :id ":id") ^:interceptors [check-auth parse-id check-folder-owner] + {:post `folder/add-character}] + [(route-map/path-for route-map/dnd-e5-char-folder-character-route :id ":id" :character-id ":character-id") ^:interceptors [check-auth parse-id check-folder-owner] + {:delete `folder/remove-character}] [(route-map/path-for route-map/login-route) {:post `login}] - ["/code/fb" - {:get `fb-oauth-code}] - ["/code/google" - {:get `google-oauth-code}] - [(route-map/path-for route-map/fb-login-route) - {:post `fb-login}] - #_[(route-map/path-for route-map/google-login-route) - {:get `google-login}] [(route-map/path-for route-map/character-pdf-route) {:post `character-pdf-2}] [(route-map/path-for route-map/verify-route) diff --git a/src/clj/orcpub/routes/folder.clj b/src/clj/orcpub/routes/folder.clj new file mode 100644 index 000000000..f04f28992 --- /dev/null +++ b/src/clj/orcpub/routes/folder.clj @@ -0,0 +1,59 @@ +(ns orcpub.routes.folder + (:require [datomic.api :as d] + [orcpub.dnd.e5.folder :as folder] + [orcpub.entity.strict :as se])) + +(def pull-folder + [:db/id ::folder/name {::folder/character-ids [:db/id ::se/owner ::se/summary]}]) + +(defn create-folder [{:keys [conn identity] folder-data :transit-params}] + ;; Whitelist only ::folder/name from client data; owner is always server-set + (let [username (:user identity) + result @(d/transact conn [{::folder/name (::folder/name folder-data) + ::folder/owner username}]) + new-id (-> result :tempids first val)] + {:status 200 :body (d/pull (d/db conn) '[*] new-id)})) + +(defn folders [{:keys [db identity]}] + (let [username (:user identity) + result (d/q [:find `(~'pull ~'?e ~pull-folder) + :in '$ '?username + :where ['?e ::folder/owner '?username]] + db username) + mapped (map (fn [[f]] + (update f ::folder/character-ids + (fn [chars] + (map (fn [{:keys [:db/id ::se/owner ::se/summary]}] + (assoc summary :db/id id ::se/owner owner)) + chars)))) + result)] + {:status 200 :body mapped})) + +(defn update-folder-name [{:keys [conn] + folder-name :transit-params + {:keys [id]} :path-params}] + @(d/transact conn [{:db/id id ::folder/name folder-name}]) + {:status 200 :body (d/pull (d/db conn) pull-folder id)}) + +(defn add-character [{:keys [db conn] + character-id :transit-params + {:keys [id]} :path-params}] + ;; Enforce at-most-one-folder: retract from existing folder first + (when-let [existing-id (d/q '[:find ?f . + :in $ ?char + :where [?f :orcpub.dnd.e5.folder/character-ids ?char]] + db character-id)] + @(d/transact conn [[:db/retract existing-id ::folder/character-ids character-id]])) + @(d/transact conn [{:db/id id ::folder/character-ids character-id}]) + {:status 200 :body (d/pull (d/db conn) '[*] id)}) + +(defn remove-character [{:keys [conn] + {:keys [id character-id]} :path-params}] + @(d/transact conn [[:db/retract id ::folder/character-ids (Long/parseLong character-id)]]) + {:status 200 :body (d/pull (d/db conn) '[*] id)}) + +(defn delete-folder [{:keys [conn] + {:keys [id]} :path-params}] + ;; retractEntity removes only the folder entity; referenced character entities are unaffected + @(d/transact conn [[:db/retractEntity id]]) + {:status 200}) diff --git a/src/clj/orcpub/routes/party.clj b/src/clj/orcpub/routes/party.clj index a3152f1d9..751960a89 100644 --- a/src/clj/orcpub/routes/party.clj +++ b/src/clj/orcpub/routes/party.clj @@ -1,16 +1,35 @@ (ns orcpub.routes.party + "HTTP route handlers for party management operations. + + Provides CRUD operations for D&D parties with error handling." (:require [clojure.spec.alpha :as spec] [datomic.api :as d] [orcpub.dnd.e5.party :as party] - [orcpub.entity.strict :as se])) + [orcpub.entity.strict :as se] + [orcpub.errors :as errors])) + +(defn create-party + "Creates a new party owned by the authenticated user. + + Args: + request - HTTP request map with: + :conn - Database connection + :identity - Authenticated user identity + :transit-params - Party data -(defn create-party [{:keys [db conn identity] party :transit-params}] - (if (spec/valid? ::party/party party) - (let [username (:user identity) - result @(d/transact conn [(assoc party ::party/owner username)]) - new-id (-> result :tempids first val)] - {:status 200 :body (d/pull (d/db conn) '[*] new-id)}) - {:status 400})) + Returns: + HTTP response with created party data + + Throws: + ExceptionInfo on database failure with :party-creation-failed error code" + [{:keys [db conn identity] party :transit-params}] + (let [username (:user identity)] + (errors/with-db-error-handling :party-creation-failed + {:username username} + "Unable to create party. Please try again or contact support." + (let [result @(d/transact conn [(assoc party ::party/owner username)]) + new-id (-> result :tempids first val)] + {:status 200 :body (d/pull (d/db conn) '[*] new-id)})))) (def pull-party [:db/id ::party/name {::party/character-ids [:db/id ::se/owner ::se/summary]}]) @@ -37,27 +56,74 @@ {:status 200 :body mapped})) -(defn update-party-name [{:keys [db conn identity] - party-name :transit-params - {:keys [id]} :path-params}] - @(d/transact conn [{:db/id id - ::party/name party-name}]) - {:status 200 - :body (d/pull (d/db conn) pull-party id)}) +(defn update-party-name + "Updates a party's name. + + Args: + request - HTTP request with party name and party ID + + Returns: + HTTP response with updated party data + + Throws: + ExceptionInfo on database failure with :party-update-failed error code" + [{:keys [db conn identity] + party-name :transit-params + {:keys [id]} :path-params}] + (errors/with-db-error-handling :party-update-failed + {:party-id id} + "Unable to update party name. Please try again or contact support." + @(d/transact conn [{:db/id id + ::party/name party-name}]) + {:status 200 + :body (d/pull (d/db conn) pull-party id)})) (defn add-character [{:keys [db conn identity] character-id :transit-params {:keys [id]} :path-params}] - @(d/transact conn [{:db/id id - ::party/character-ids character-id}]) - {:status 200 :body (d/pull db '[*] id)}) + (try + @(d/transact conn [{:db/id id + ::party/character-ids character-id}]) + {:status 200 :body (d/pull db '[*] id)} + (catch Exception e + (println "ERROR: Failed to add character" character-id "to party" id ":" (.getMessage e)) + (throw (ex-info "Unable to add character to party. Please try again or contact support." + {:error :party-add-character-failed + :party-id id + :character-id character-id} + e))))) + +(defn remove-character + "Removes a character from a party. + + Args: + request - HTTP request with party ID and character ID + + Returns: + HTTP response with updated party data -(defn remove-character [{:keys [db conn identity] - {:keys [id character-id]} :path-params}] - @(d/transact conn [[:db/retract id ::party/character-ids (Long/parseLong character-id)]]) - {:status 200 :body (d/pull db '[*] id)}) + Throws: + ExceptionInfo on invalid character ID or database failure" + [{:keys [db conn identity] + {:keys [id character-id]} :path-params}] + (let [char-id (errors/with-validation :invalid-character-id + {:character-id character-id} + "Invalid character ID format" + (Long/parseLong character-id))] + (errors/with-db-error-handling :party-remove-character-failed + {:party-id id :character-id char-id} + "Unable to remove character from party. Please try again or contact support." + @(d/transact conn [[:db/retract id ::party/character-ids char-id]]) + {:status 200 :body (d/pull db '[*] id)}))) (defn delete-party [{:keys [db conn identity] {:keys [id]} :path-params}] - @(d/transact conn [[:db/retractEntity id]]) - {:status 200}) + (try + @(d/transact conn [[:db/retractEntity id]]) + {:status 200} + (catch Exception e + (println "ERROR: Failed to delete party" id ":" (.getMessage e)) + (throw (ex-info "Unable to delete party. Please try again or contact support." + {:error :party-deletion-failed + :party-id id} + e))))) diff --git a/src/clj/orcpub/security.clj b/src/clj/orcpub/security.clj index c6d7c3696..e1207304a 100644 --- a/src/clj/orcpub/security.clj +++ b/src/clj/orcpub/security.clj @@ -1,5 +1,5 @@ (ns orcpub.security - (:require [clj-time.core :as t :refer [ago now minutes hours]])) + (:require [clj-time.core :as t :refer [ago minutes]])) (defn compare-dates [attempt-1 attempt-2] (compare (:date attempt-1) (:date attempt-2))) @@ -54,10 +54,10 @@ @failed-login-attempts-by-username)) (defn usernames-for-attempts [attempts] - (into #{} (map :user attempts))) + (set (map :user attempts))) (defn ips-for-attempts [attempts] - (into #{} (map :ip attempts))) + (set (map :ip attempts))) (defn multiple-account-access-aux [ip attempts-by-ip] (some-> ip @@ -81,6 +81,6 @@ (>= 3))) (defn multiple-ip-attempts-to-same-account? [username] - multiple-ip-attempts-to-same-account-aux + (multiple-ip-attempts-to-same-account-aux username - @failed-login-attempts-by-username) + @failed-login-attempts-by-username)) diff --git a/src/clj/orcpub/styles/core.clj b/src/clj/orcpub/styles/core.clj index 342a114e9..33af9aa84 100644 --- a/src/clj/orcpub/styles/core.clj +++ b/src/clj/orcpub/styles/core.clj @@ -1,14 +1,16 @@ (ns orcpub.styles.core - (:require [garden.def :refer [defstylesheet defstyles]] - [garden.stylesheet :refer [at-media at-keyframes]] + (:require [garden.stylesheet :refer [at-media at-keyframes]] [garden.units :refer [px]] [orcpub.constants :as const] [garden.selectors :as s])) +;; Color palette — used across UI for consistent theming (def orange "#f0a100") (def button-color orange) (def red "#9a031e") (def green "#70a800") +(def cyan "#47eaf8") ; import log, conflict rename option +(def purple "#8b7ec8") ; conflict skip option (def container-style {:display :flex @@ -93,19 +95,23 @@ [:.list-style-disc {:list-style-type :disc :list-style-position :inside}] - + [:.f-w-bold {:font-weight :bold}] - + [:.flex-grow-1 {:flex-grow 1}] [:.flex-basis-50-p {:flex-basis "50%"}] - + [:.i {:font-style :italic}] - + + [:.wsp-prw + {:white-space "pre-wrap" + :display "block"}] + [:.f-w-n {:font-weight :normal}] [:.f-w-b @@ -134,7 +140,7 @@ {:margin-right "20px"}] [:.m-r-30 {:margin-right "30px"}] - + [:.m-r-80 {:margin-right "80px"}] @@ -193,6 +199,8 @@ [:.m-b-40 {:margin-bottom "40px"}] + [:.m-l-2 + {:margin-left "2px"}] [:.m-l--10 {:margin-left "-10px"}] [:.m-l--5 @@ -223,7 +231,7 @@ [:.orange-shadow {:box-shadow "0 1px 0 0 #f0a100"}] - + [:.t-a-c {:text-align :center}] [:.t-a-l @@ -249,6 +257,12 @@ [:.w-auto {:width :auto}] + [:.w-10-p + {:width "10%"}] + [:.w-20-p + {:width "20%"}] + [:.w-30-p + {:width "30%"}] [:.w-40-p {:width "40%"}] [:.w-50-p @@ -298,7 +312,7 @@ [:.overflow-auto {:overflow :auto}] - + [:.posn-rel {:position :relative}] [:.posn-abs @@ -417,7 +431,7 @@ [:.b-b-2 {:border-bottom "2px solid"}] - + [:.b-w-3 {:border-width "3px"}] [:.b-w-5 @@ -431,6 +445,43 @@ [:.hidden {:display :none}] + [:.invisible + {:visibility :hidden}] + + [:.tooltip + {:position "relative" + :display "inline-block" + :border-bottom "1px dotted black"}] + + [:.tooltip [:.tooltiptext + {:visibility "hidden" + :width "130px" + :bottom "calc(100% - -5px)" + :left "50%" + :margin-left "-60px" + :background-color "black" + :font-family "Open Sans, sans-serif" + :font-size "14px" + :font-weight "normal" + :color "#fff" + :text-align "center" + :padding "10px 10px" + :border-radius "6px" + :position "absolute" + :z-index "1"}]] + + [:.tooltip:hover [:.tooltiptext + {:visibility "visible"}]] + + [:.image-character-thumbnail + {:max-height "100px" + :max-width "200px" + :border-radius "5px"}] + + [:.image-faction-thumbnail + {:max-height "100px" + :max-width "200px" + :border-radius "5px"}] (at-keyframes :fade-out @@ -471,7 +522,7 @@ [:.hover-opacity-full:hover {:opacity 1.0}] - + [:.bg-light {:background-color "rgba(72,72,72,0.2)"}] [:.bg-lighter @@ -483,6 +534,15 @@ [:.bg-green {:background-color "#70a800"}] + ;; Warning/alert styles + [:.bg-warning + {:background-color "rgba(240, 161, 0, 0.1)" + :border "1px solid rgba(240, 161, 0, 0.3)" + :border-radius "4px"}] + [:.bg-warning-item + {:background-color "rgba(0, 0, 0, 0.2)" + :border-radius "4px"}] + [:.fade-out {:animation-name :fade-out :animation-duration :5s}] @@ -718,27 +778,29 @@ xs-query [:.user-icon {:display :none}] - [:.character-builder-header - #_{:margin-bottom 0}] - [:.list-character-summary - {:font-size "18px"}] - [:.character-summary - {:flex-wrap :wrap}] - [:.app-header - {:height :auto - :background-image :none - :background-color "rgba(0, 0, 0, 0.3)" - :min-height 0}] - [:.app-header-bar - {:min-height (px 50) - :backdrop-filter :none - :-webkit-backdrop-filter :none}] - [:.content - {:width "100%"}] - #_[:.options-column - {:width "100%"}] - [:.header-button-text :.header-links - {:display :none}]) + [:.character-builder-header + #_{:margin-bottom 0}] + [:.list-character-summary + {:font-size "18px"}] + [:.character-summary + {:flex-wrap :wrap}] + [:.app-header + {:height :auto + :background-image :none + :background-color "rgba(0, 0, 0, 0.3)" + :min-height 0}] + [:.app-header-bar + {:min-height (px 50) + :backdrop-filter :none + :-webkit-backdrop-filter :none}] + [:.app-header-menu + {:flex-grow 1}] + [:.content + {:width "100%"}] + #_[:.options-column + {:width "100%"}] + [:.header-button-text :.header-links + {:display :none}]) #_(at-media xs-query @@ -820,11 +882,15 @@ (def app (concat - [ - - [:.character-builder-header + [[:.character-builder-header {:margin-bottom "19px"}] + [:.senses + {:width "450px"}] + + [:.notes + {:width "350px"}] + [:.registration-content {:width "785px" :min-height "600px"}] @@ -836,23 +902,23 @@ {:margin "10px 0"}] #_["input::-webkit-outer-spin-button" - "input::-webkit-inner-spin-button" - {:-webkit-appearance :none - :margin 0}] + "input::-webkit-inner-spin-button" + {:-webkit-appearance :none + :margin 0}] #_["input[type=number]" - {:-moz-appearance :textfield}] + {:-moz-appearance :textfield}] [:a :a:visited {:color orange}] [:select {:font-family font-family - :cursor :pointer}] + :color "white" + :background-color :transparent}] [:*:focus {:outline 0}] - [:.sticky-header {:top 0 @@ -877,9 +943,9 @@ [:.header-tab {:background-color "rgba(0, 0, 0, 0.5)" - ;;:-webkit-backdrop-filter "blur(5px)" - ;;:backdrop-filter "blur(5px)" - }] + :-webkit-backdrop-filter "blur(3px)" + :backdrop-filter "blur(3px)" + :border-radius "5px"}] [:.header-tab.mobile [:.title @@ -896,8 +962,8 @@ {:border-bottom "1px solid rgba(255,255,255,0.5)"}] #_[:.header-tab:hover - [(garden.selectors/& (garden.selectors/not :.disabled)) - {:background-color orange}]] + [(garden.selectors/& (garden.selectors/not :.disabled)) + {:background-color orange}]] [:.app-header-bar {:min-height (px 81) @@ -906,7 +972,7 @@ :background-color "rgba(0, 0, 0, 0.25)"}] #_[:.options-column - {:width "300px"}] + {:width "300px"}] [:.builder-column {:display :none @@ -943,7 +1009,7 @@ :border-bottom "5px solid rgba(72,72,72,0.37)"} [:.builder-tab-text {:opacity 0.2}]] - + [:.selected-builder-tab {:border-bottom-color "#f1a20f"} [:.builder-tab-text @@ -996,7 +1062,7 @@ [:.remove-item-button {:color button-color - :font-size "16px" + :font-size "16px" :margin-left "5px" :cursor :pointer}] @@ -1045,12 +1111,28 @@ :cursor :pointer :background-image "linear-gradient(to bottom, #f1a20f, #dbab50)"}] + [:.roll-button + {:color :white + :min-width "68px" + :font-weight 600 + :font-size "14px" + :border :none + :border-radius "2px" + :padding "6px 6px" + :margin-right "2px" + :margin-left "2px" + :margin-bottom "2px" + :margin-top "2px" + :cursor :pointer + :background-image "linear-gradient(to bottom, #f1a20f, #dbab50)"}] + [:.form-button:hover {:box-shadow "0 2px 6px 0 rgba(0, 0, 0, 0.5)"}] [:.form-button.disabled {:opacity 0.5 - :cursor :not-allowed}] + :cursor :not-allowed + :pointer-events "none"}] [:.form-button.disabled:hover {:box-shadow :none}] @@ -1114,6 +1196,39 @@ [:.checkbox-text {:margin-left "5px"}] + ;; Character filter bar — scoped styles for dropdowns and checkboxes + [:.char-filter-bar + [:.filter-dropdown + {:position :absolute + :background-color "#313A4D" + :padding "6px 4px" + :top "100%" + :margin-top "4px" + :border "1px solid rgba(255,255,255,0.15)" + :border-radius "4px" + :max-height "300px" + :overflow-y :auto + :font-weight :normal + :font-size "14px" + :z-index 200 + :box-shadow "0 4px 12px rgba(0,0,0,0.4)"}] + [:.filter-dropdown-item + {:padding "6px 10px" + :border-radius "3px" + :cursor :pointer}] + [:.filter-dropdown-item:hover + {:background-color "rgba(255,255,255,0.08)"}] + [:.checkbox + {:width "14px" + :height "14px" + :min-width "14px" + :flex-shrink 0} + [:.fa-check + {:font-size "12px"}]] + [:.flex.pointer + {:align-items :center + :gap "8px"}]] + [:#selection-stepper {:transition "top 2s ease-in-out" :width "240px" @@ -1150,6 +1265,11 @@ [:.app.light-theme {:background-image "linear-gradient(182deg, #FFFFFF, #DDDDDD)"} + [:select + {:font-family font-family + :color "black"; + :background-color :transparent}] + [:.item-list {:border-top "1px solid rgba(0,0,0,0.5)"}] @@ -1166,16 +1286,16 @@ {:stroke "#363636"}] [:.input - {:background-color :transparent - :color :black - :border "1px solid #282828" - :border-radius "5px" - :margin-top "5px" - :display :block - :padding "10px" - :width "100%" - :box-sizing :border-box - :font-size "14px"}] + {:background-color :transparent + :color :black + :border "1px solid #282828" + :border-radius "5px" + :margin-top "5px" + :display :block + :padding "10px" + :width "100%" + :box-sizing :border-box + :font-size "14px"}] [:.form-button {:background-image "linear-gradient(to bottom, #33658A, #33658A)"}] @@ -1215,10 +1335,235 @@ [:table.striped [:tr [(s/& (s/nth-child :even)) - {:background-color "rgba(0, 0, 0, 0.1)"}]]]]] + {:background-color "rgba(0, 0, 0, 0.1)"}]]]] + + ;;;; "Modal" styles + [:.modal-container + {:background-image "linear-gradient(to right, #d35730, #eda41e)" + :padding ".5em 2em"}] + + [:.modal-container :.m-b-10, + :.modal-container :.link-button + {:font-weight "bold"}] + + [:.modal-container :.link-button + {:color "#f7c257" + ;:font-weight "bold" + }] + + ;;;; WARNING TOOLTIP "warntip" + [:.warntiptext + {:width "20%" + :margin-top "10px" + :background-color "#d94b20" + :color "#fff" + :text-align "center" + :padding "5px 0" + :border-radius "0 0 6px 6px" + :position "absolute" + :border "solid 1px #e96868" + :z-index 1}] + + [:.warntip :.warntiptext + [:&:after + {:content "\" \"" + :position "absolute" + :bottom "100%" ;; At the bottom of the tooltip + :left "50%" + :margin-left "-5px" + :border-width "10px" + :border-style "solid" + :border-color "transparent transparent #e96868 transparent"}]] + + ;;;; CONFLICT RESOLUTION MODAL + + ;; Modal structure + [:.conflict-backdrop + {:position :fixed + :top 0 :left 0 :right 0 :bottom 0 + :background "rgba(0,0,0,0.6)" + :z-index 10001 + :display :flex + :align-items :center + :justify-content :center}] + + [:.conflict-modal + {:background "#1a1e28" + :border-radius "5px" + :max-width "600px" + :max-height "80vh" + :overflow :hidden + :display :flex + :flex-direction :column + :box-shadow "0 2px 6px 0 rgba(0,0,0,0.5)"}] + + [:.conflict-modal-header + {:padding "16px 20px" + :border-bottom "1px solid rgba(255,255,255,0.15)" + :background "#2c3445"}] + + [:.conflict-modal-footer + {:padding "16px 20px" + :border-top "1px solid rgba(255,255,255,0.15)" + :display :flex + :justify-content :flex-end + :gap "12px"}] + + [:.conflict-modal-body + {:padding "16px 20px" + :overflow-y :auto + :flex 1}] + + ;; Header elements + [:.conflict-title-icon + {:color orange + :font-size "18px"}] + + [:.conflict-title + {:color orange}] + + [:.conflict-subtitle + {:color "rgba(255,255,255,0.5)" + :margin-top "4px"}] + + [:.conflict-count + {:color "rgba(255,255,255,0.5)" + :margin-top "8px"}] + + ;; Conflict card + [:.conflict-item + {:background "rgba(255,255,255,0.07)" + :border-radius "0 5px 5px 0" + :padding "12px" + :margin-bottom "8px" + :border "1px solid rgba(255,255,255,0.12)" + :border-left (str "3px solid " orange)}] + + [:.conflict-item-header + {:margin-bottom "10px"}] + + [:.conflict-item-key + {:color orange}] + + [:.conflict-item-type + {:color "rgba(255,255,255,0.7)" + :margin-left "8px"}] + + [:.conflict-item-desc + {:color "rgba(255,255,255,0.7)" + :margin-bottom "8px"}] + + [:.conflict-item-detail + {:margin-left "12px"}] + + [:.conflict-source-import + {:color cyan + :font-weight :bold}] + + [:.conflict-source-existing + {:color green + :font-weight :bold}] + + [:.conflict-source-label + {:color "rgba(255,255,255,0.5)"}] + + [:.conflict-source-origin + {:color "rgba(255,255,255,0.35)"}] + + [:.conflict-source-row + {:margin-bottom "6px" + :color :white}] + + ;; Resolution options section + [:.conflict-options + {:margin-top "12px" + :border-top "1px solid rgba(255,255,255,0.2)" + :padding-top "12px"}] + + [:.conflict-options-label + {:color "rgba(255,255,255,0.7)" + :margin-bottom "10px" + :text-transform :uppercase + :letter-spacing "0.5px" + :font-weight :bold + :font-size "12px"}] + + ;; Radio option — base (unselected) + [:.conflict-radio + {:margin-bottom "8px" + :padding "8px 8px 8px 12px" + :background "rgba(255,255,255,0.04)" + :border-left "3px solid rgba(255,255,255,0.1)" + :border-radius "0 5px 5px 0" + :cursor :pointer + :transition "background 0.15s ease, border-color 0.15s ease" + :color "rgba(255,255,255,0.7)"} + [:.radio-icon + {:color "rgba(255,255,255,0.35)" + :font-size "16px" + :margin-right "10px" + :width "16px"}]] + + ;; Radio option — selected (shared) + [:.conflict-radio.selected + {:color "rgba(255,255,255,0.95)"}] + + ;; Radio option — rename variant (cyan) + [:.conflict-radio-rename.selected + {:border-left (str "3px solid " cyan) + :background (str cyan "18")} + [:.radio-icon + {:color cyan}]] + + ;; Radio option — keep variant (orange) + [:.conflict-radio-keep.selected + {:border-left (str "3px solid " orange) + :background (str orange "18")} + [:.radio-icon + {:color orange}]] + + ;; Radio option — skip variant (purple) + [:.conflict-radio-skip.selected + {:border-left (str "3px solid " purple) + :background (str purple "18")} + [:.radio-icon + {:color purple}]] + + ;; Code block in rename option + [:.conflict-code + {:background "rgba(0,0,0,0.3)" + :padding "3px 8px" + :border-radius "3px" + :margin-left "6px" + :color cyan + :font-weight :bold}] + + ;; Export warning modal reuses conflict-backdrop, conflict-modal, + ;; conflict-modal-header, conflict-modal-footer, conflict-modal-body + + [:.export-issue-type + {:color "rgba(255,255,255,0.7)" + :margin-bottom "6px" + :font-weight :bold}] + + [:.export-issue-item + {:color "rgba(255,255,255,0.5)" + :font-size "12px" + :margin-bottom "4px"}] + + [:.export-issue-name + {:color "rgba(255,255,255,0.8)"}] + + [:.export-issue-missing + {:color orange + :margin-left "8px"}]];concat-bracket margin-lefts margin-tops widths font-sizes props - media-queries)) + media-queries) ;concat +);def app + + + ;;);concat;app diff --git a/src/clj/orcpub/system.clj b/src/clj/orcpub/system.clj index 1399bddd0..3101128de 100644 --- a/src/clj/orcpub/system.clj +++ b/src/clj/orcpub/system.clj @@ -1,8 +1,8 @@ (ns orcpub.system (:require [com.stuartsierra.component :as component] - [reloaded.repl :refer [init start stop go reset]] - [io.pedestal.http :as http] - [orcpub.pedestal :as pedestal] + [reloaded.repl :as rrepl] + [io.pedestal.http :as http] + [orcpub.pedestal :as pedestal] [orcpub.routes :as routes] [orcpub.datomic :as datomic] [environ.core :as environ]) @@ -22,7 +22,14 @@ {::http/routes routes/routes ::http/type :jetty ::http/port (let [port-str (System/getenv "PORT")] - (if port-str (Integer/parseInt port-str))) + (when port-str + (try + (Integer/parseInt port-str) + (catch NumberFormatException e + (throw (ex-info "Invalid PORT environment variable. Expected a number." + {:error :invalid-port + :port port-str} + e)))))) ::http/join false ::http/resource-path "/public" ::http/container-options {:context-configurator (fn [c] @@ -35,16 +42,16 @@ :conn (datomic/new-datomic (if-let [datomic-url (:datomic-url environ/env)] - (str datomic-url "?aws_access_key_id=" (environ/env :datomic-access-key) "&aws_secret_key=" (environ/env :datomic-secret-key)) + (str datomic-url) (when true #_(= :dev env) (println "WARN: no :datomic-url environment variable set; using local dev") - "datomic:free://localhost:4334/orcpub"))) + "datomic:free://localhost:4334/orcpub?password=datomic"))) :service-map (cond-> (merge {:env env} prod-service-map - (if (= :dev env) dev-service-map-overrides)) + (when (= :dev env) dev-service-map-overrides)) true http/default-interceptors (= :dev env) http/dev-interceptors) @@ -53,4 +60,4 @@ (pedestal/new-pedestal) [:service-map :conn]))) -(reloaded.repl/set-init! #(system :prod)) +(rrepl/set-init! #(system :prod)) diff --git a/src/clj/orcpub/tools/orcbrew.clj b/src/clj/orcpub/tools/orcbrew.clj new file mode 100644 index 000000000..52109768d --- /dev/null +++ b/src/clj/orcpub/tools/orcbrew.clj @@ -0,0 +1,261 @@ +(ns orcpub.tools.orcbrew + "Command-line tools for inspecting and debugging orcbrew files. + + Usage: + lein prettify-orcbrew - Pretty-print EDN + lein prettify-orcbrew --analyze - Show potential issues + + Version: 0.01" + (:require [clojure.edn :as edn] + [clojure.pprint :as pp] + [clojure.string :as str] + [clojure.java.io :as io])) + +(def version "0.02") + +;;; ============================================================ +;;; Analysis functions - detect potential issues WITHOUT fixing them +;;; ============================================================ + +(defn find-nil-nil-patterns + "Find {nil nil, ...} patterns in raw string content." + [content] + (let [matches (re-seq #"nil\s+nil\s*," content)] + {:count (count matches) + :pattern "nil nil," + :description "Spurious nil key-value pairs (e.g., {nil nil, :key :foo})"})) + +(def problematic-unicode + "Map of problematic Unicode characters and their descriptions." + {;; Quotation marks + \u2018 "left single quote" + \u2019 "right single quote" + \u201A "single low-9 quote" + \u201B "single high-reversed-9 quote" + \u201C "left double quote" + \u201D "right double quote" + \u201E "double low-9 quote" + \u201F "double high-reversed-9 quote" + \u2032 "prime (feet)" + \u2033 "double prime (inches)" + ;; Dashes + \u2010 "hyphen" + \u2011 "non-breaking hyphen" + \u2012 "figure dash" + \u2013 "en-dash" + \u2014 "em-dash" + \u2015 "horizontal bar" + ;; Spaces + \u00A0 "non-breaking space" + \u2002 "en space" + \u2003 "em space" + \u2009 "thin space" + \u200A "hair space" + \u200B "zero-width space" + \u202F "narrow no-break space" + ;; Other + \u2026 "ellipsis" + \u2022 "bullet" + \u2212 "minus sign" + \u00D7 "multiplication sign" + \u00F7 "division sign" + \u00AE "registered trademark" + \u00A9 "copyright" + \u2122 "trademark"}) + +(defn find-problematic-unicode + "Find all problematic Unicode characters that should be ASCII." + [content] + (let [found (for [[char desc] problematic-unicode + :let [pattern (re-pattern (java.util.regex.Pattern/quote (str char))) + matches (re-seq pattern content)] + :when (seq matches)] + {:char char + :code (int char) + :description desc + :count (count matches)})] + found)) + +(defn find-other-non-ascii + "Find any non-ASCII characters not in our known problematic set." + [content] + (let [known-chars (set (keys problematic-unicode)) + non-ascii (filter #(and (> (int %) 127) + (not (known-chars %))) + content) + grouped (frequencies non-ascii)] + (when (seq grouped) + (for [[char cnt] grouped] + {:char char + :code (int char) + :description "unknown non-ASCII" + :count cnt})))) + +(defn find-disabled-entries + "Find :disabled? patterns which indicate commented-out content." + [content] + (let [matches (re-seq #":disabled\?\s+true" content)] + {:count (count matches) + :pattern ":disabled? true" + :description "Entries marked as disabled (typically errors in original)"})) + +(defn analyze-traits + "Check traits for missing :name fields in parsed data." + [data path] + (let [results (atom [])] + (letfn [(check-traits [m current-path] + (when (map? m) + (doseq [[k v] m] + (cond + ;; Found a :traits vector + (and (= k :traits) (vector? v)) + (doseq [[idx trait] (map-indexed vector v)] + (when (and (map? trait) + (:description trait) + (not (:name trait))) + (swap! results conj + {:path (conj current-path :traits idx) + :issue "Trait missing :name field" + :description (subs (:description trait) 0 + (min 50 (count (:description trait))))}))) + + ;; Recurse into maps + (map? v) + (check-traits v (conj current-path k)) + + ;; Recurse into map values that are maps + :else nil))))] + (check-traits data [])) + @results)) + +(defn analyze-content + "Analyze raw content for potential issues (before parsing)." + [content] + (println "\n=== Content Analysis ===") + (println (str "File size: " (count content) " bytes")) + + ;; nil nil patterns + (let [{:keys [count pattern description]} (find-nil-nil-patterns content)] + (when (pos? count) + (println (str "\n[WARNING] Found " count " '" pattern "' patterns")) + (println (str " " description)))) + + ;; Problematic Unicode (known replaceable) + (let [unicode-issues (find-problematic-unicode content)] + (when (seq unicode-issues) + (let [total (reduce + (map :count unicode-issues))] + (println (str "\n[WARNING] Found " total " problematic Unicode characters (will be auto-fixed on import):")) + (doseq [{:keys [description code count]} (sort-by :count > unicode-issues)] + (println (str " - " description " (U+" (format "%04X" code) "): " count " occurrences")))))) + + ;; Unknown non-ASCII (not in our replacement map) + (let [unknown (find-other-non-ascii content)] + (when (seq unknown) + (let [total (reduce + (map :count unknown))] + (println (str "\n[WARNING] Found " total " unknown non-ASCII characters (may need manual review):")) + (doseq [{:keys [char code count]} (take 10 (sort-by :count > unknown))] + (println (str " - U+" (format "%04X" code) " '" char "': " count " occurrences"))) + (when (> (clojure.core/count unknown) 10) + (println (str " ... and " (- (clojure.core/count unknown) 10) " more unique characters")))))) + + ;; Disabled entries + (let [{:keys [count]} (find-disabled-entries content)] + (when (pos? count) + (println (str "\n[INFO] Found " count " disabled entries (previously errored content)"))))) + +(defn analyze-data + "Analyze parsed data structure for potential issues." + [data] + (println "\n=== Structure Analysis ===") + + ;; Check if multi-plugin format + (let [multi? (and (map? data) + (every? string? (keys data)))] + (if multi? + (do + (println (str "Format: Multi-plugin (" (count data) " sources)")) + (doseq [source (keys data)] + (println (str " - \"" source "\"")))) + (println "Format: Single-plugin"))) + + ;; Check for traits without names + (let [missing-names (analyze-traits data [])] + (when (seq missing-names) + (println (str "\n[WARNING] Found " (count missing-names) " traits missing :name field:")) + (doseq [{:keys [path description]} (take 10 missing-names)] + (println (str " - " (pr-str path))) + (println (str " \"" description "...\""))) + (when (> (count missing-names) 10) + (println (str " ... and " (- (count missing-names) 10) " more")))))) + +;;; ============================================================ +;;; Main functions +;;; ============================================================ + +(defn prettify-file + "Read an orcbrew file and pretty-print it." + [filepath] + (let [content (slurp filepath) + data (edn/read-string content)] + (pp/pprint data))) + +(defn prettify-to-file + "Read an orcbrew file and write prettified version to output file." + [input-path output-path] + (let [content (slurp input-path) + data (edn/read-string content)] + (with-open [w (io/writer output-path)] + (pp/pprint data w)) + (println (str "Wrote prettified output to: " output-path)))) + +(defn analyze-file + "Analyze an orcbrew file for potential issues without modifying it." + [filepath] + (println (str "Analyzing: " filepath)) + (println (str "Tool version: " version)) + (let [content (slurp filepath)] + ;; Analyze raw content first + (analyze-content content) + + ;; Parse and analyze structure + (try + (let [data (edn/read-string content)] + (analyze-data data)) + (catch Exception e + (println (str "\n[ERROR] Failed to parse EDN: " (.getMessage e))))))) + +(defn -main + "Entry point for lein run." + [& args] + (let [filepath (first args) + analyze? (some #{"--analyze" "-a"} args) + output (some #(when (str/starts-with? % "--output=") + (subs % 9)) args)] + (cond + (nil? filepath) + (do + (println "Usage: lein prettify-orcbrew [options]") + (println "") + (println "Options:") + (println " --analyze, -a Analyze file for potential issues") + (println " --output= Write prettified output to file") + (println "") + (println "Examples:") + (println " lein prettify-orcbrew my-content.orcbrew") + (println " lein prettify-orcbrew my-content.orcbrew --analyze") + (println " lein prettify-orcbrew my-content.orcbrew --output=pretty.edn") + (throw (ex-info "No filepath provided" {:type :usage-error}))) + + (not (.exists (io/file filepath))) + (do + (println (str "Error: File not found: " filepath)) + (throw (ex-info (str "File not found: " filepath) {:type :file-not-found :filepath filepath}))) + + analyze? + (analyze-file filepath) + + output + (prettify-to-file filepath output) + + :else + (prettify-file filepath)))) diff --git a/src/cljc/orcpub/common.cljc b/src/cljc/orcpub/common.cljc index 6546ea1a0..3f968357d 100644 --- a/src/cljc/orcpub/common.cljc +++ b/src/cljc/orcpub/common.cljc @@ -6,7 +6,7 @@ (def dot-char "•") (defn- name-to-kw-aux [name ns] - (if (string? name) + (when (string? name) (as-> name $ (s/lower-case $) (s/replace $ #"'" "") @@ -20,7 +20,7 @@ (memoized-name-to-kw name ns)) (defn kw-to-name [kw & [capitalize?]] - (if (keyword? kw) + (when (keyword? kw) (as-> kw $ (name $) (s/split $ #"\-") @@ -36,17 +36,18 @@ (defn map-by-id [values] (map-by :db/id values)) -(defmacro ptime [message body] +;; dead — zero callers +#_(defmacro ptime [message body] `(do (prn ~message) (time ~body))) (defn bonus-str [val] - (str (if (pos? val) "+") val)) + (str (when (pos? val) "+") val)) (defn mod-str [val] - (cond (pos? val) (str " + " val) - (neg? val) (str " - " (int (Math/abs val))) - :else "")) + (cond (pos? val) (str "+" val) + (neg? val) (str "-" (int (Math/abs val))) + :else (str "+" val))) (defn map-vals [val-fn m] (reduce-kv @@ -63,7 +64,7 @@ 2 (s/join (str " " preceding-last " ") list) (str (s/join ", " (butlast list)) - (str ", " preceding-last " ") + ", " preceding-last " " (last list))))) (defn round-up [num] @@ -79,19 +80,40 @@ (warn (str "non-keyword value passed to safe-name: " kw)))) (defn safe-capitalize [s] - (if (string? s) (s/capitalize s))) + (when (string? s) (s/capitalize s))) (defn safe-capitalize-kw [kw] (some-> kw name safe-capitalize)) +(defn kw-base + "Extract the base part of a keyword (before first dash). + E.g., :artificer-kibbles-tasty -> \"artificer\"" + [kw] + (when (keyword? kw) + (first (s/split (name kw) #"-")))) + +(defn traverse-nested + "HOF for traversing nested option structures (vector/map/nil pattern). + Calls (f item path) for each nested item, returns concatenated results." + [f coll path] + (mapcat + (fn [[k v]] + (cond + (vector? v) + (apply concat (map-indexed (fn [idx item] (f item (conj path k idx))) v)) + (map? v) + (f v (conj path k)) + :else nil)) + coll)) + (defn sentensize [desc] - (if desc + (when desc (str (s/upper-case (subs desc 0 1)) (subs desc 1) - (if (not (s/ends-with? desc ".")) + (when (not (s/ends-with? desc ".")) ".")))) (def add-keys-xform @@ -143,20 +165,23 @@ (vec (keep-indexed (fn [i item] - (if (not= i index) + (when (not= i index) item)) v))) (def rounds-per-minute 10) (def minutes-per-hour 60) -(def hours-per-day 24) +;; dead — redefined in views.cljs, never used from common +#_(def hours-per-day 24) (def rounds-per-hour (* minutes-per-hour rounds-per-minute)) -(defn rounds-to-hours [rounds] +;; dead — zero callers +#_(defn rounds-to-hours [rounds] (int (/ rounds rounds-per-hour))) -(defn rounds-to-minutes [rounds] +;; dead — zero callers +#_(defn rounds-to-minutes [rounds] (int (/ (rem rounds rounds-per-hour) rounds-per-minute))) (def filter-true-xform @@ -177,3 +202,20 @@ (fn [[k v]] (str (safe-capitalize-kw k) " " (bonus-str v))) m))) +;; Case Insensitive `sort-by` +(defn aloof-sort-by [sorter coll] + (sort-by (fn [x] + (let [v (sorter x)] + (cond + (string? v) (s/lower-case v) + (nil? v) "" + :else (s/lower-case (str v))))) + coll) + ) + +(defn ->kebab-case [s] + (when (string? s) + (-> s + ;; Insert hyphen before each capital letter, but not at the start. + (s/replace #"([A-Z])" "-$1") + (s/lower-case)))) diff --git a/src/cljc/orcpub/components.cljc b/src/cljc/orcpub/components.cljc index a9bb3d7bf..d4971590d 100644 --- a/src/cljc/orcpub/components.cljc +++ b/src/cljc/orcpub/components.cljc @@ -1,13 +1,12 @@ (ns orcpub.components - (:require [re-frame.core :refer [dispatch]] - [clojure.string :as s] + (:require [clojure.string :as s] #?(:cljs [reagent.core :refer [atom]]))) (defn checkbox [selected? disable?] [:i.fa.fa-check.f-s-14.bg-white.b-color-gray.orange-shadow.pointer.b-1 {:class-name (str (if selected? "black slight-text-shadow" "white transparent") " " - (if disable? + (when disable? "opacity-5"))}]) (defn labeled-checkbox [label selected? disabled? on-click] @@ -21,20 +20,26 @@ {:value key} name]) +;; Form-2 component: resets "] - (doall - (map - (fn [{:keys [key name]}] - ^{:key key} - [selection-item key name false]) - values))]) + (let [selected-value (atom "")] + (fn [values on-change] + [:select.builder-option.builder-option-dropdown + {:value @selected-value + :on-change (fn [e] + (let [v (-> e .-target .-value)] + (on-change e) + (reset! selected-value "")))} + [:option.builder-dropdown-item + {:value "" + :disabled true} + "" :disabled? true :value :select}] @@ -5059,15 +5277,15 @@ obj-to-item selections) [{:title "" - :value :new-selection}]) + :value :new-selection}])) :value (or type :select) :on-change #(if (= "new-selection" %) (dispatch [::selections/new-selection]) (dispatch [edit-selection-type-event index (keyword %)]))}]] - (if type + (when type [:div.m-t-10.m-l-5 [modifier-level-selector index level edit-selection-level-event]]) - (if type + (when type [:div.m-t-10.m-l-5 [labeled-dropdown "Amount to Select at this Level" @@ -5076,7 +5294,7 @@ (range 1 11)) :value (or num 1) :on-change #(dispatch [edit-selection-num-event index (js/parseInt %)])}]]) - (if (or type level num) + (when (or type level num) [:div.m-t-10 [:button.form-button.m-l-5 {:on-click #(dispatch [delete-selection-event index])} @@ -5089,13 +5307,13 @@ edit-modifier-level-event delete-modifier-event] (let [mod-values (modifier-values) - {:keys [name values component value-fn]} (if type (mod-values type))] + {:keys [name values component value-fn]} (when type (mod-values type))] [:div [:div.flex.flex-wrap.align-items-end.m-b-20 [:div.m-t-10 [labeled-dropdown "Modifier Type" - {:items (cons + {:items (sort-by :title (cons {:title "