diff --git a/builtwithdjango/analytics.py b/builtwithdjango/analytics.py index 4ceeb70..08b682e 100644 --- a/builtwithdjango/analytics.py +++ b/builtwithdjango/analytics.py @@ -24,6 +24,7 @@ def posthog_template_context(request): return { + "app_environment": getattr(settings, "ENVIRONMENT", ""), "posthog_enabled": getattr(settings, "POSTHOG_ENABLED", False), "posthog_api_key": getattr(settings, "POSTHOG_API_KEY", ""), "posthog_api_host": getattr(settings, "POSTHOG_HOST", "https://us.i.posthog.com"), diff --git a/frontend/src/application/hotwire.js b/frontend/src/application/hotwire.js index a89789a..a1fbc17 100644 --- a/frontend/src/application/hotwire.js +++ b/frontend/src/application/hotwire.js @@ -3,7 +3,6 @@ import "../styles/tailwind.css"; import { Application } from "@hotwired/stimulus"; import { definitionsFromContext } from "@hotwired/stimulus-webpack-helpers"; -import Dropdown from 'stimulus-dropdown'; import Reveal from 'stimulus-reveal-controller'; @@ -12,5 +11,4 @@ const application = Application.start(); const context = require.context("../controllers", true, /\.js$/); application.load(definitionsFromContext(context)); -application.register('dropdown', Dropdown); application.register('reveal', Reveal); diff --git a/frontend/src/controllers/dropdown_controller.js b/frontend/src/controllers/dropdown_controller.js new file mode 100644 index 0000000..ec61a9c --- /dev/null +++ b/frontend/src/controllers/dropdown_controller.js @@ -0,0 +1,47 @@ +import Dropdown from "stimulus-dropdown"; + +export default class extends Dropdown { + connect() { + super.connect(); + this.boundCloseOnEscape = this.closeOnEscape.bind(this); + document.addEventListener("keydown", this.boundCloseOnEscape); + this.setExpanded(false); + } + + disconnect() { + if (typeof super.disconnect === "function") { + super.disconnect(); + } + document.removeEventListener("keydown", this.boundCloseOnEscape); + } + + toggle(event) { + super.toggle(event); + window.requestAnimationFrame(() => { + this.setExpanded(!this.menuTarget.classList.contains("hidden")); + }); + } + + hide(event) { + super.hide(event); + if (!this.element.contains(event.target)) { + this.setExpanded(false); + } + } + + closeOnEscape(event) { + if (event.key !== "Escape" || this.menuTarget.classList.contains("hidden")) return; + + this.leave(); + this.setExpanded(false); + this.triggerElement?.focus(); + } + + setExpanded(isExpanded) { + this.triggerElement?.setAttribute("aria-expanded", isExpanded ? "true" : "false"); + } + + get triggerElement() { + return this.element.querySelector("[aria-expanded]"); + } +} diff --git a/frontend/src/controllers/exit_intent_controller.js b/frontend/src/controllers/exit_intent_controller.js index 098be9c..c6eedc5 100644 --- a/frontend/src/controllers/exit_intent_controller.js +++ b/frontend/src/controllers/exit_intent_controller.js @@ -1,32 +1,31 @@ import { Controller } from "@hotwired/stimulus"; -import {enter, leave} from 'el-transition'; +import { enter, leave } from "el-transition"; export default class extends Controller { - static targets = [ "modal", "closeButton" ]; + static targets = ["modal", "closeButton"]; - initialize() { - this.numOfOpens = 0; - } + initialize() { + this.numOfOpens = 0; + } - openModal(event) { - if(this.modalTarget.classList.contains('hidden') && this.numOfOpens === 0) { - if (!event.toElement && !event.relatedTarget) { - setTimeout(() => { - enter(this.modalTarget); - this.numOfOpens++; - if (window.bwdTrack) { - window.bwdTrack('exit intent modal opened'); - } - }, 1000); - } + openModal(event) { + if (this.modalTarget.classList.contains("hidden") && this.numOfOpens === 0) { + if (!event.toElement && !event.relatedTarget) { + setTimeout(() => { + enter(this.modalTarget); + this.numOfOpens++; + if (window.bwdTrack) { + window.bwdTrack("exit intent modal opened"); + } + }, 1000); } } + } - closeModal() { - console.log("not hidden"); - leave(this.modalTarget); - if (window.bwdTrack) { - window.bwdTrack('exit intent modal closed'); - } + closeModal() { + leave(this.modalTarget); + if (window.bwdTrack) { + window.bwdTrack("exit intent modal closed"); } + } } diff --git a/frontend/src/controllers/html_formatter_controller.js b/frontend/src/controllers/html_formatter_controller.js index 8a882c5..b8d54f6 100644 --- a/frontend/src/controllers/html_formatter_controller.js +++ b/frontend/src/controllers/html_formatter_controller.js @@ -1,46 +1,54 @@ import { Controller } from "@hotwired/stimulus"; export default class extends Controller { - static targets = [ "input", "result", "submitButton", "copyButton" ]; + static targets = [ "input", "result", "submitButton", "copyButton", "status" ]; formatHTML() { + const input = this.inputTarget.value.trim(); + if (!input) { + this.setError('Paste a Django template before formatting.'); + return; + } + this.setLoading(true); + this.setStatus('Formatting template...'); const formData = new FormData(); - formData.append('html_string', this.inputTarget.value); + formData.append('html_string', input); fetch('/tools/api/format-html/', { method: 'POST', body: formData, }) - .then(response => { + .then(async response => { if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); + const data = await response.json().catch(() => ({})); + throw new Error(data.error || `Request failed with status ${response.status}`); } return response.json(); }) .then(data => { if (data.formatted_html) { this.resultTarget.value = data.formatted_html; - this.copyButtonTarget.classList.remove('hidden'); + this.copyButtonTarget.disabled = false; + this.resetCopyButton(); + this.setStatus('Formatted HTML is ready.'); this.capture('html formatted', { - input_length: this.inputTarget.value.length, + input_length: input.length, output_length: data.formatted_html.length }); } else if (data.error) { - this.resultTarget.value = 'Error: ' + data.error; - this.copyButtonTarget.classList.add('hidden'); + this.setError(data.error); this.capture('html formatter failed', { - input_length: this.inputTarget.value.length, + input_length: input.length, error: data.error }); } }) .catch(error => { - this.resultTarget.value = 'Error: ' + error; - this.copyButtonTarget.classList.add('hidden'); + this.setError(error.message || 'Could not format HTML. Check your connection and try again.'); this.capture('html formatter failed', { - input_length: this.inputTarget.value.length, + input_length: input.length, error: error.message }); }) @@ -49,10 +57,24 @@ export default class extends Controller { }); } - copy() { - navigator.clipboard.writeText(this.resultTarget.value); - this.copyButtonTarget.classList.replace("bg-blue-500", "bg-green-500"); + async copy() { + if (!this.resultTarget.value) { + this.setError('Format HTML before copying.'); + return; + } + + try { + await this.copyText(this.resultTarget.value); + } catch (error) { + this.setError('Could not copy automatically. Select the result and copy it manually.'); + this.capture('formatted html copy failed', { + error: error.message + }); + return; + } + this.copyButtonTarget.textContent = "Copied!"; + this.setStatus('Formatted HTML copied.'); this.capture('formatted html copied', { output_length: this.resultTarget.value.length }); @@ -60,20 +82,56 @@ export default class extends Controller { } resetCopyButton() { - this.copyButtonTarget.classList.replace("bg-green-500", "bg-blue-500"); this.copyButtonTarget.textContent = "Copy"; } setLoading(isLoading) { this.submitButtonTarget.disabled = isLoading; this.submitButtonTarget.textContent = isLoading ? 'Formatting...' : 'Format HTML'; - this.submitButtonTarget.classList.toggle('opacity-50', isLoading); - this.submitButtonTarget.classList.toggle('cursor-not-allowed', isLoading); + } + + setStatus(message) { + this.statusTarget.textContent = message; + this.statusTarget.classList.add('bw-muted'); + this.statusTarget.classList.remove('bw-status-error'); + } + + setError(message) { + this.resultTarget.value = ''; + this.copyButtonTarget.disabled = true; + this.statusTarget.textContent = message; + this.statusTarget.classList.remove('bw-muted'); + this.statusTarget.classList.add('bw-status-error'); + } + + async copyText(value) { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(value); + return; + } + + const textarea = document.createElement('textarea'); + textarea.value = value; + textarea.setAttribute('readonly', ''); + textarea.style.position = 'fixed'; + textarea.style.top = '-9999px'; + document.body.appendChild(textarea); + textarea.select(); + + try { + const copied = document.execCommand('copy'); + if (!copied) { + throw new Error('Copy command was rejected'); + } + } finally { + textarea.remove(); + } } clearResult() { this.resultTarget.value = ''; - this.copyButtonTarget.classList.add('hidden'); + this.copyButtonTarget.disabled = true; + this.setStatus('Result cleared.'); this.capture('html formatter cleared'); } diff --git a/frontend/src/controllers/infinite_scroll_controller.js b/frontend/src/controllers/infinite_scroll_controller.js index be5b1d7..c19cd9f 100755 --- a/frontend/src/controllers/infinite_scroll_controller.js +++ b/frontend/src/controllers/infinite_scroll_controller.js @@ -51,7 +51,6 @@ export default class extends Controller { } }) .catch((error) => { - console.error('Error:', error); if (window.bwdTrack) { window.bwdTrack('infinite scroll failed', { url, diff --git a/frontend/src/controllers/like_controller.js b/frontend/src/controllers/like_controller.js index 7f1b54d..5f6b176 100644 --- a/frontend/src/controllers/like_controller.js +++ b/frontend/src/controllers/like_controller.js @@ -38,8 +38,7 @@ export default class extends Controller { this.render(); this.trackChange(this.likedValue, previousLikeCount); }) - .catch((error) => { - console.log(error); + .catch(() => { this.showError(); }) .finally(() => { diff --git a/frontend/src/controllers/search_controller.js b/frontend/src/controllers/search_controller.js index c07144b..15553ee 100644 --- a/frontend/src/controllers/search_controller.js +++ b/frontend/src/controllers/search_controller.js @@ -68,10 +68,10 @@ export default class extends Controller { updateSelection(results) { results.forEach((result, index) => { if (index === this.selectedIndex) { - result.classList.add("bg-gray-50"); + result.classList.add("bw-search-result--active"); result.scrollIntoView({ block: "nearest" }); } else { - result.classList.remove("bg-gray-50"); + result.classList.remove("bw-search-result--active"); } }); } @@ -116,7 +116,6 @@ export default class extends Controller { has_results: data.length > 0 }); } catch (error) { - console.error("Search error:", error); this.capture("project search failed", { query_length: query.length, error: error.message @@ -128,7 +127,7 @@ export default class extends Controller { showResults(results) { if (!results.length) { const emptyState = document.createElement("div"); - emptyState.className = "p-4 text-sm text-gray-500"; + emptyState.className = "bw-search-empty"; emptyState.textContent = "No projects found"; this.resultsTarget.replaceChildren(emptyState); this.resultsTarget.classList.remove("hidden"); @@ -146,14 +145,14 @@ export default class extends Controller { link.dataset.analyticsProjectId = String(result.id || ""); link.dataset.analyticsProjectTitle = result.title || ""; link.dataset.analyticsProjectSlug = result.slug || ""; - link.className = "flex flex-col p-4 border-b border-gray-100 hover:bg-gray-50 last:border-b-0"; + link.className = "bw-search-result"; const title = document.createElement("div"); - title.className = "font-medium text-gray-900"; + title.className = "bw-search-result__title"; title.textContent = result.title || ""; const description = document.createElement("div"); - description.className = "text-sm text-gray-500"; + description.className = "bw-search-result__description"; description.textContent = result.short_description || ""; link.append(title, description); diff --git a/frontend/src/controllers/secret_key_controller.js b/frontend/src/controllers/secret_key_controller.js index da387cc..32cd751 100644 --- a/frontend/src/controllers/secret_key_controller.js +++ b/frontend/src/controllers/secret_key_controller.js @@ -1,56 +1,114 @@ import { Controller } from "@hotwired/stimulus"; export default class extends Controller { - static targets = [ "output", "copyButton" ]; + static targets = [ "output", "copyButton", "generateButton", "status" ]; static values = { url: String }; - generate() { + async generate() { if (!this.urlValue) { - console.error('URL is undefined'); - this.outputTarget.value = 'Error: URL is not set'; + this.setStatus("Secret key generation is not configured. Try again later.", true); return; } - fetch(this.urlValue, { - method: 'POST', - headers: { - 'X-CSRFToken': this.getCookie('csrftoken'), - 'Content-Type': 'application/json', - }, - body: JSON.stringify({}), - }) - .then(response => { + this.setGenerating(true); + this.setStatus("Generating a new secret key..."); + + try { + const response = await fetch(this.urlValue, { + method: 'POST', + headers: { + 'X-CSRFToken': this.getCookie('csrftoken'), + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + }); + if (!response.ok) { - throw new Error('Network response was not ok'); + throw new Error(`Request failed with status ${response.status}`); } - return response.json(); - }) - .then(data => { + + const data = await response.json(); + if (!data.secret_key) { + throw new Error("The server did not return a secret key"); + } + this.outputTarget.value = data.secret_key; - this.resetCopyButton(); // Reset the copy button when new secret is generated + this.copyButtonTarget.disabled = false; + this.resetCopyButton(); + this.setStatus("Secret key generated. Copy it before leaving this page."); this.capture('django secret generated'); - }) - .catch(error => { - console.error('Error:', error); - this.outputTarget.value = 'An error occurred: ' + error.message; + } catch (error) { + this.outputTarget.value = ""; + this.copyButtonTarget.disabled = true; + this.setStatus("Could not generate a secret key. Check your connection and try again.", true); this.capture('django secret generation failed', { error: error.message }); - }); + } finally { + this.setGenerating(false); + } } - copy() { - navigator.clipboard.writeText(this.outputTarget.value); - this.copyButtonTarget.classList.replace("bg-green-600", "bg-blue-600"); + async copy() { + if (!this.outputTarget.value) { + this.setStatus("Generate a secret key before copying.", true); + return; + } + + try { + await this.copyText(this.outputTarget.value); + } catch (error) { + this.setStatus("Could not copy automatically. Select the key and copy it manually.", true); + this.capture('django secret copy failed', { + error: error.message + }); + return; + } + this.copyButtonTarget.textContent = "Copied"; + this.setStatus("Secret key copied."); this.capture('django secret copied'); } resetCopyButton() { - this.copyButtonTarget.classList.replace("bg-blue-600", "bg-green-600"); this.copyButtonTarget.textContent = "Copy"; } + setGenerating(isGenerating) { + this.generateButtonTarget.disabled = isGenerating; + this.generateButtonTarget.textContent = isGenerating ? "Generating..." : "Generate Secret Key"; + } + + setStatus(message, isError = false) { + this.statusTarget.textContent = message; + this.statusTarget.classList.toggle("bw-muted", !isError); + this.statusTarget.classList.toggle("bw-status-error", isError); + } + + async copyText(value) { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(value); + return; + } + + const textarea = document.createElement("textarea"); + textarea.value = value; + textarea.setAttribute("readonly", ""); + textarea.style.position = "fixed"; + textarea.style.top = "-9999px"; + document.body.appendChild(textarea); + textarea.select(); + + try { + const copied = document.execCommand("copy"); + if (!copied) { + throw new Error("Copy command was rejected"); + } + } finally { + textarea.remove(); + } + } + getCookie(name) { let cookieValue = null; if (document.cookie && document.cookie !== '') { diff --git a/frontend/src/sentry.js b/frontend/src/sentry.js index cd76956..9f10166 100644 --- a/frontend/src/sentry.js +++ b/frontend/src/sentry.js @@ -1,5 +1,3 @@ -import * as Sentry from "@sentry/browser"; - const SENSITIVE_QUERY_PARTS = [ "token", "auth", @@ -70,9 +68,12 @@ function scrubEvent(event) { return event; } -const config = readSentryConfig(); +async function initializeSentry() { + const config = readSentryConfig(); + + if (!config.enabled || !config.dsn) return; -if (config.enabled && config.dsn) { + const Sentry = await import(/* webpackChunkName: "sentry" */ "@sentry/browser"); const tracePropagationTargets = Array.isArray(config.tracePropagationTargets) && config.tracePropagationTargets.length ? config.tracePropagationTargets @@ -107,3 +108,7 @@ if (config.enabled && config.dsn) { Sentry.setUser(config.user); } } + +initializeSentry().catch((error) => { + void error; +}); diff --git a/frontend/src/styles/tailwind.css b/frontend/src/styles/tailwind.css index 331a528..2edfe10 100644 --- a/frontend/src/styles/tailwind.css +++ b/frontend/src/styles/tailwind.css @@ -169,11 +169,17 @@ box-shadow 150ms var(--bw-ease); } - .bw-button-primary { - background: var(--bw-primary-dark); - color: oklch(98% 0.01 118); - box-shadow: 0 8px 18px oklch(27% 0.09 154 / 0.18); - } + .bw-button:disabled { + cursor: not-allowed; + opacity: 0.58; + transform: none; + } + + .bw-button-primary { + background: var(--bw-primary-dark); + color: oklch(98% 0.01 118); + box-shadow: 0 8px 18px oklch(27% 0.09 154 / 0.18); + } .bw-button-primary:hover { background: var(--bw-primary); @@ -203,6 +209,10 @@ transform: translateY(-1px); } + .bw-button:disabled:hover { + transform: none; + } + .bw-kicker { display: inline-flex; align-items: center; @@ -259,6 +269,8 @@ .bw-brand { display: inline-flex; + min-width: 2.75rem; + min-height: 2.75rem; align-items: center; gap: 0.65rem; color: var(--bw-ink); @@ -273,7 +285,7 @@ .bw-nav-link { display: inline-flex; - min-height: 2.5rem; + min-height: 2.75rem; align-items: center; border-radius: var(--bw-radius); padding-inline: 0.75rem; @@ -422,12 +434,12 @@ line-height: 1.2; } - @media (hover: hover) { - .bw-hero-placeholder:hover { - border-color: var(--bw-accent); - box-shadow: 0 10px 20px oklch(13% 0.05 156 / 0.16); - transform: translateY(-2px); - } + @media (hover: hover) { + .bw-hero-placeholder:hover { + border-color: var(--bw-accent); + box-shadow: 0 10px 20px oklch(13% 0.05 156 / 0.16); + transform: translateY(-2px); + } .bw-hero-placeholder:hover .bw-hero-placeholder__action { color: var(--bw-ink); @@ -542,8 +554,8 @@ .bw-icon-button { display: inline-flex; - min-width: 2.5rem; - min-height: 2.5rem; + min-width: 2.75rem; + min-height: 2.75rem; align-items: center; justify-content: center; border: 1px solid var(--bw-border); @@ -567,6 +579,121 @@ transform: translateY(-1px); } + .bw-ad-mobile { + position: fixed; + inset-inline: 0; + bottom: 0; + z-index: 50; + border-top: 1px solid var(--bw-border-strong); + background: oklch(98.8% 0.012 112); + box-shadow: 0 -16px 38px oklch(16% 0.04 151 / 0.18); + opacity: 1; + transform: translateY(100%); + transition: transform 220ms var(--bw-ease); + } + + .bw-ad-mobile__inner { + display: flex; + width: min(100% - 2rem, 72rem); + align-items: center; + justify-content: space-between; + gap: 0.75rem; + margin-inline: auto; + padding-block: 0.75rem; + } + + .bw-ad-mobile__link { + display: flex; + min-width: 0; + align-items: center; + gap: 0.75rem; + text-decoration: none; + } + + .bw-ad-desktop { + position: fixed; + right: 1rem; + bottom: 1rem; + z-index: 50; + width: min(20rem, calc(100vw - 2rem)); + border: 1px solid var(--bw-border-strong); + border-radius: var(--bw-radius); + background: oklch(98.8% 0.012 112); + background-clip: padding-box; + box-shadow: 0 24px 60px oklch(16% 0.04 151 / 0.26), 0 1px 0 oklch(100% 0.01 118) inset; + isolation: isolate; + opacity: 1; + padding: 1rem; + transform: translateY(0.5rem); + transition: transform 220ms var(--bw-ease); + } + + .bw-ad--visible { + transform: translateY(0); + } + + .bw-ad__logo { + display: flex; + flex-shrink: 0; + align-items: center; + justify-content: center; + border: 1px solid var(--bw-border); + border-radius: var(--bw-radius); + background: oklch(99% 0.01 118); + transition: border-color 150ms var(--bw-ease), transform 150ms var(--bw-ease); + } + + .bw-ad__logo:hover { + border-color: var(--bw-primary); + transform: translateY(-1px); + } + + .bw-ad__logo--mobile { + width: 2.5rem; + height: 2.5rem; + } + + .bw-ad__logo--desktop { + width: 3.5rem; + height: 3.5rem; + } + + .bw-ad__logo-image { + object-fit: contain; + } + + .bw-ad__logo-image--mobile { + width: 1.75rem; + height: 1.75rem; + } + + .bw-ad__logo-image--desktop { + width: 2.5rem; + height: 2.5rem; + } + + .bw-ad__close-desktop { + position: absolute; + top: 0.5rem; + right: 0.5rem; + width: 2rem; + min-width: 2rem; + height: 2rem; + min-height: 2rem; + } + + @media (max-width: 1023px) { + .bw-ad-desktop { + display: none; + } + } + + @media (min-width: 1024px) { + .bw-ad-mobile { + display: none; + } + } + .bw-project-screenshot { display: block; width: 100%; @@ -611,6 +738,44 @@ } } + .bw-search-result { + display: flex; + flex-direction: column; + gap: 0.2rem; + border-bottom: 1px solid var(--bw-border); + padding: 1rem; + color: var(--bw-ink); + text-decoration: none; + transition: background-color 150ms var(--bw-ease); + } + + .bw-search-result:last-child { + border-bottom: 0; + } + + .bw-search-result:hover, + .bw-search-result--active { + background: var(--bw-primary-soft); + } + + .bw-search-result__title { + color: var(--bw-ink); + font-weight: 750; + line-height: 1.25; + } + + .bw-search-result__description { + color: var(--bw-muted); + font-size: 0.875rem; + line-height: 1.35; + } + + .bw-search-empty { + padding: 1rem; + color: var(--bw-muted); + font-size: 0.875rem; + } + .bw-settings-section { display: grid; gap: 1.25rem; @@ -648,7 +813,8 @@ color: var(--bw-ink); font-size: 0.875rem; line-height: 1.25rem; - padding: 0.625rem 0.75rem; + min-height: 2.75rem; + padding: 0.7rem 0.75rem; } .bw-form-input::placeholder { @@ -660,6 +826,25 @@ box-shadow: 0 0 0 3px oklch(51% 0.15 151 / 0.18); } + .bw-inline-form { + display: flex; + width: 100%; + max-width: 28rem; + align-items: stretch; + } + + .bw-inline-form .bw-form-input { + margin-top: 0; + border-start-end-radius: 0; + border-end-end-radius: 0; + } + + .bw-inline-form .bw-button { + flex-shrink: 0; + border-start-start-radius: 0; + border-end-start-radius: 0; + } + .bw-form-checkbox { width: 1rem; height: 1rem; @@ -687,6 +872,11 @@ list-style: none; } + .bw-status-error { + color: var(--bw-danger); + font-weight: 750; + } + .bw-prose { color: var(--bw-ink-soft); } @@ -708,6 +898,24 @@ color: oklch(91% 0.025 126); } + .bw-footer-brand { + display: inline-flex; + min-height: 2.75rem; + align-items: center; + gap: 0.75rem; + color: inherit; + font-weight: 850; + text-decoration: none; + } + + .bw-footer-link { + display: flex; + min-height: 2.75rem; + align-items: center; + color: inherit; + text-decoration: none; + } + .bw-footer a:hover { color: var(--bw-accent); } @@ -777,10 +985,54 @@ min-width: 0; } - .btn-blue { - @apply inline-flex items-center px-4 py-2 font-semibold text-white bg-blue-500 rounded-lg shadow-md; - @apply hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-blue-400 focus:ring-opacity-75; + .bw-webring-fallback { + display: grid; + gap: 0.55rem; + } + + .bw-webring-fallback a { + display: inline-flex; + min-height: 3.25rem; + flex-direction: column; + align-items: flex-start; + justify-content: center; + gap: 0.18rem; + border: 1px solid oklch(43% 0.05 150); + border-radius: var(--bw-radius); + background: oklch(17% 0.032 151); + color: oklch(93% 0.025 126); + padding: 0.7rem 0.78rem; + font-size: 0.86rem; + font-weight: 900; + line-height: 1.05; + text-decoration: none; + transition: + background-color 160ms var(--bw-ease), + border-color 160ms var(--bw-ease), + color 160ms var(--bw-ease), + transform 160ms var(--bw-ease); + } + + .bw-webring-fallback a span + span { + color: oklch(70% 0.025 126); + font-size: 0.7rem; + font-weight: 750; + line-height: 1.1; } + + .bw-webring-fallback a:hover, + .bw-webring-fallback a:focus-visible { + border-color: oklch(76% 0.15 92); + background: oklch(23% 0.052 151); + color: oklch(97% 0.018 116); + transform: translateY(-1px); + } + + .bw-webring-fallback a:hover span + span, + .bw-webring-fallback a:focus-visible span + span { + color: oklch(83% 0.07 92); + } + } @media (prefers-reduced-motion: reduce) { diff --git a/frontend/webpack/webpack.common.js b/frontend/webpack/webpack.common.js index ffe8651..1bacdf6 100644 --- a/frontend/webpack/webpack.common.js +++ b/frontend/webpack/webpack.common.js @@ -32,7 +32,16 @@ module.exports = { new CleanWebpackPlugin(), new CopyWebpackPlugin({ patterns: [ - { from: Path.resolve(__dirname, "../vendors"), to: "vendors" }, + { + from: Path.resolve(__dirname, "../vendors"), + to: "vendors", + globOptions: { + ignore: [ + "**/images/sample.jpg", + "**/images/webpack.png", + ], + }, + }, ], }), new WebpackAssetsManifest({ diff --git a/frontend/webpack/webpack.config.prod.js b/frontend/webpack/webpack.config.prod.js index db68385..4a8b156 100644 --- a/frontend/webpack/webpack.config.prod.js +++ b/frontend/webpack/webpack.config.prod.js @@ -40,6 +40,14 @@ module.exports = merge(common, { chunkFilename: "css/[id].[contenthash].css", }), ].concat(sentryPluginEnabled ? [sentryWebpackPlugin(sentryPluginOptions)] : []), + performance: { + maxAssetSize: 244 * 1024, + maxEntrypointSize: 244 * 1024, + assetFilter: (assetFilename) => { + if (/\.map$/.test(assetFilename)) return false; + return !/^js\/sentry\.[a-f0-9]+\.chunk\.js$/.test(assetFilename); + }, + }, module: { rules: [ { diff --git a/newsletter/forms.py b/newsletter/forms.py index 3a15836..8e77cf9 100755 --- a/newsletter/forms.py +++ b/newsletter/forms.py @@ -12,7 +12,7 @@ def __init__(self, *args, **kwargs): self.fields[fieldname].help_text = None self.fields[fieldname].widget.attrs.update( { - "class": "block appearance-none w-full bg-white border border-solid border-grey-light hover:border-grey px-2 py-2 rounded-l-lg shadow" + "class": "block min-h-[2.75rem] w-full appearance-none rounded-lg border border-solid border-grey-light bg-white px-3 py-3 text-base shadow hover:border-grey" } ) self.fields[fieldname].widget.attrs["placeholder"] = "email@mail.com" diff --git a/pages/forms.py b/pages/forms.py index 3fc1679..340980b 100644 --- a/pages/forms.py +++ b/pages/forms.py @@ -12,19 +12,19 @@ def __init__(self, *args, **kwargs): super(AddNftRequest, self).__init__(*args, **kwargs) self.fields["email"].widget.attrs.update( { - "class": "block w-full border-0 p-1 bg-gray-100 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-md", + "class": "bw-form-input text-base", "placeholder": "test@example.com", } ) self.fields["wallet_public_key"].widget.attrs.update( { - "class": "block w-full border-0 p-1 bg-gray-100 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-md", + "class": "bw-form-input text-base", "placeholder": "0x123456789651bE2ad266e9ca6A9d50686B183e49", } ) self.fields["date_requested"].widget.attrs.update( { - "class": "block w-full border-0 p-1 bg-gray-100 text-gray-900 placeholder-gray-500 focus:ring-0 sm:text-md", + "class": "bw-form-input text-base", "placeholder": "2021-11-23", } ) diff --git a/projects/views.py b/projects/views.py index d80e099..b14025f 100755 --- a/projects/views.py +++ b/projects/views.py @@ -56,6 +56,7 @@ def get_context_data(self, **kwargs): class InactiveProjectListView(LoginRequiredMixin, UserPassesTestMixin, ListView): + login_url = "account_login" model = Project template_name = "projects/all_inactive_projects.html" diff --git a/tailwind.config.js b/tailwind.config.js index db83f49..efedb9f 100755 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,12 +1,19 @@ module.exports = { content: [ './templates/**/*.html', + './frontend/src/**/*.js', ], - // https://nexxai.dev/tell-purgecss-to-ignore-purging-all-tailwind-colours/ + // Dynamic classes that come from template data rather than static markup. safelist: [ - {pattern: /(bg|text)-(.*)-(.*)/}, - {pattern: /(w)-(.*)/}, - {pattern: /(h)-(.*)/}, + 'h-12', + 'h-20', + 'h-40', + 'w-12', + 'w-20', + 'w-40', + { + pattern: /^(bg|text)-(slate|gray|zinc|neutral|stone|red|orange|amber|yellow|lime|green|emerald|teal|cyan|sky|blue|indigo|violet|purple|fuchsia|pink|rose)-(50|100|200|300|400|500|600|700|800|900)$/, + }, ], theme: { extend: { diff --git a/templates/account/login.html b/templates/account/login.html index dbdb908..6b43627 100755 --- a/templates/account/login.html +++ b/templates/account/login.html @@ -10,12 +10,12 @@
-

+

Sign in to your account -

-

+ +

Or - + sign up if you don't have one yet.

@@ -28,20 +28,20 @@

{{ form.login.errors | safe }} - {% render_field form.login placeholder="Username" id="username" name="username" type="text" autocomplete="username" required=True class="block relative px-3 py-2 w-full placeholder-gray-500 text-gray-900 rounded-none rounded-t-md border border-gray-300 appearance-none focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" %} + {% render_field form.login placeholder="Username" id="username" name="username" type="text" autocomplete="username" required=True class="block relative min-h-[2.75rem] px-3 py-2 w-full placeholder:text-[var(--bw-muted)] text-[var(--bw-ink)] bg-[var(--bw-surface)] rounded-none rounded-t-md border border-[var(--bw-border)] appearance-none focus:outline-none focus:ring-[var(--bw-primary)] focus:border-[var(--bw-primary)] focus:z-10 sm:text-sm" %}
{{ form.password.errors | safe }} - {% render_field form.password id="password" name="password" type="password" autocomplete="current-password" required="True" class="block relative px-3 py-2 w-full placeholder-gray-500 text-gray-900 rounded-none rounded-b-md border border-gray-300 appearance-none focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 focus:z-10 sm:text-sm" placeholder="Confirm Password" %} + {% render_field form.password id="password" name="password" type="password" autocomplete="current-password" required="True" class="block relative min-h-[2.75rem] px-3 py-2 w-full placeholder:text-[var(--bw-muted)] text-[var(--bw-ink)] bg-[var(--bw-surface)] rounded-none rounded-b-md border border-[var(--bw-border)] appearance-none focus:outline-none focus:ring-[var(--bw-primary)] focus:border-[var(--bw-primary)] focus:z-10 sm:text-sm" placeholder="Password" %}

@@ -66,14 +53,14 @@

- Or continue with + Or continue with
{% if 'twitter' in available_social_providers %}
+ class="bw-button bw-button-secondary w-full"> Sign in with Twitter

{% if 'github' in available_social_providers %}
+ class="bw-button bw-button-secondary w-full"> Sign in with GitHub
{% endif %}

-
+ + {% endblock content %} diff --git a/templates/base.html b/templates/base.html index 96b86c0..cd0bd5a 100755 --- a/templates/base.html +++ b/templates/base.html @@ -14,12 +14,12 @@ {% endblock meta %} - {% stylesheet_pack 'hotwire' attrs='data-turbo-track="reload"' %} - - + {% if app_environment == "prod" %} + + {% endif %} {% if sentry_browser_enabled %} {{ sentry_browser_config|json_script:"bwd-sentry-config" }} {% endif %} @@ -234,7 +234,7 @@