diff --git a/.gitignore b/.gitignore index a77d830..673d986 100644 --- a/.gitignore +++ b/.gitignore @@ -39,4 +39,7 @@ public/* !public/.gitkeep !public/privacy.html !public/favicon.svg +!public/ads/ !ads.txt +# Local-only backups of source mockups — don't track in git +public/ads/_originals/ diff --git a/public/ads/horizontal-mobile.jpg b/public/ads/horizontal-mobile.jpg new file mode 100644 index 0000000..7283662 Binary files /dev/null and b/public/ads/horizontal-mobile.jpg differ diff --git a/public/ads/horizontal.jpg b/public/ads/horizontal.jpg new file mode 100644 index 0000000..f00dd49 Binary files /dev/null and b/public/ads/horizontal.jpg differ diff --git a/public/ads/unisex-staple-t-shirt-black-front-6a0df0764472a.png b/public/ads/unisex-staple-t-shirt-black-front-6a0df0764472a.png new file mode 100644 index 0000000..7bda0a0 Binary files /dev/null and b/public/ads/unisex-staple-t-shirt-black-front-6a0df0764472a.png differ diff --git a/public/ads/unisex-staple-t-shirt-black-left-front-6a0df07642e27.png b/public/ads/unisex-staple-t-shirt-black-left-front-6a0df07642e27.png new file mode 100644 index 0000000..4596566 Binary files /dev/null and b/public/ads/unisex-staple-t-shirt-black-left-front-6a0df07642e27.png differ diff --git a/public/ads/unisex-staple-t-shirt-black-right-front-6a0df0763f3d0.png b/public/ads/unisex-staple-t-shirt-black-right-front-6a0df0763f3d0.png new file mode 100644 index 0000000..fc4fa7d Binary files /dev/null and b/public/ads/unisex-staple-t-shirt-black-right-front-6a0df0763f3d0.png differ diff --git a/public/ads/unisex-staple-t-shirt-black-right-front-6a0df07641fa5.png b/public/ads/unisex-staple-t-shirt-black-right-front-6a0df07641fa5.png new file mode 100644 index 0000000..6b54166 Binary files /dev/null and b/public/ads/unisex-staple-t-shirt-black-right-front-6a0df07641fa5.png differ diff --git a/src/App.js b/src/App.js index 33bd2be..2abede7 100644 --- a/src/App.js +++ b/src/App.js @@ -3,6 +3,7 @@ import InputSection from './components/InputSection'; import OutputSection from './components/OutputSection'; import TabSelector from './components/TabSelector'; import Toast from './components/Toast'; +import TshirtAds from './components/TshirtAds'; import { convertToJson } from './utils/converter'; import './styles/variables.css'; import './styles/layout.css'; @@ -10,6 +11,7 @@ import './styles/editor.css'; import './styles/toolbar.css'; import './styles/components.css'; import './styles/responsive.css'; +import './styles/ads.css'; function App() { const [input, setInput] = useState(''); @@ -65,7 +67,8 @@ function App() {

Python Dict to JSON Converter

Paste your Python dictionary to convert it to valid JSON format instantly

- + + {isMobile && ( )} diff --git a/src/components/TshirtAds.js b/src/components/TshirtAds.js new file mode 100644 index 0000000..0671233 --- /dev/null +++ b/src/components/TshirtAds.js @@ -0,0 +1,139 @@ +import React, { useState, useEffect } from 'react'; +import { createPortal } from 'react-dom'; + +const FOURTHWALL_URL = 'https://raisin-pains-shop.fourthwall.com/products/raisin-pains-supersoft-sycophancy-tee'; + +const DISMISS_KEY = 'tshirt-ad-dismissed-at'; +const DISMISS_TTL_MS = 7 * 24 * 60 * 60 * 1000; +const TSHIRT_ALT = "Programmer's dict-to-JSON t-shirt — Shop on Fourthwall"; + +// Pools of images per ad size. Add more filenames here as you produce them. +// On each page load, a random non-overlapping selection is made for each size. +const IMAGE_POOLS = { + '160x600': [ + 'unisex-staple-t-shirt-black-front-6a0df0764472a.png', + 'unisex-staple-t-shirt-black-left-front-6a0df07642e27.png', + 'unisex-staple-t-shirt-black-right-front-6a0df0763f3d0.png', + 'unisex-staple-t-shirt-black-right-front-6a0df07641fa5.png', + ], + '728x90': [ + 'horizontal.jpg', + ], + '320x50': [ + 'horizontal-mobile.jpg', + ], +}; + +const SIZES = { + '160x600': { width: 160, height: 600 }, + '728x90': { width: 728, height: 90 }, + '320x50': { width: 320, height: 50 }, +}; + +const SLOTS = [ + { position: 'left', size: '160x600' }, + { position: 'right', size: '160x600' }, + { position: 'bottom', size: '728x90' }, + { position: 'bottom-mobile', size: '320x50' }, +]; + +function shuffle(arr) { + const a = [...arr]; + for (let i = a.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [a[i], a[j]] = [a[j], a[i]]; + } + return a; +} + +// For each ad size, shuffle the pool and assign distinct images to each slot. +// If the pool is smaller than the number of slots for that size, the assignment +// wraps around (duplicates allowed only as a fallback). +function assignImages() { + const shuffled = {}; + const cursor = {}; + for (const size of Object.keys(IMAGE_POOLS)) { + shuffled[size] = shuffle(IMAGE_POOLS[size]); + cursor[size] = 0; + } + return SLOTS.map((slot) => { + const pool = shuffled[slot.size]; + const filename = pool[cursor[slot.size] % pool.length]; + cursor[slot.size] += 1; + return { ...slot, ...SIZES[slot.size], filename }; + }); +} + +function fireEvent(name, position) { + if (typeof window !== 'undefined' && typeof window.gtag === 'function') { + window.gtag('event', name, { ad_position: position }); + } +} + +function TshirtAds() { + // null = checking storage, true = hidden, false = visible + const [dismissed, setDismissed] = useState(null); + const [assignedSlots] = useState(assignImages); + + useEffect(() => { + let hidden = false; + try { + const raw = window.localStorage.getItem(DISMISS_KEY); + const ts = raw ? parseInt(raw, 10) : 0; + if (ts && Date.now() - ts < DISMISS_TTL_MS) { + hidden = true; + } + } catch (e) { + // localStorage unavailable (private mode, blocked) — fall through and show ads + } + setDismissed(hidden); + }, []); + + const handleDismiss = (position) => { + try { + window.localStorage.setItem(DISMISS_KEY, String(Date.now())); + } catch (e) { + // ignore — still dismiss in-session + } + fireEvent('tshirt_ad_dismissed', position); + setDismissed(true); + }; + + if (dismissed !== false) return null; + + return createPortal( + <> + {assignedSlots.map((slot) => ( +
+ fireEvent('tshirt_ad_click', slot.position)} + aria-label={TSHIRT_ALT} + > + {TSHIRT_ALT} + + + Ad +
+ ))} + , + document.body + ); +} + +export default TshirtAds; diff --git a/src/styles/ads.css b/src/styles/ads.css new file mode 100644 index 0000000..30baf7b --- /dev/null +++ b/src/styles/ads.css @@ -0,0 +1,91 @@ +.tshirt-ad { + position: fixed; + z-index: 9000; + background: #fff; + border: 1px solid #e2e8f0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + font-family: var(--font-family); +} + +.tshirt-ad--left { + top: 50%; + left: 16px; + transform: translateY(-50%); +} + +.tshirt-ad--right { + top: 50%; + right: 16px; + transform: translateY(-50%); +} + +.tshirt-ad--bottom, +.tshirt-ad--bottom-mobile { + bottom: 16px; + left: 50%; + transform: translateX(-50%); +} + +.tshirt-ad__image { + display: block; + object-fit: contain; + background: #fff; +} + +.tshirt-ad__dismiss { + position: absolute; + top: 4px; + right: 4px; + width: 24px; + height: 24px; + background: rgba(255, 255, 255, 0.9); + border: 1px solid #cbd5e0; + border-radius: 50%; + cursor: pointer; + font-size: 16px; + line-height: 1; + color: #4a5568; + padding: 0; + display: flex; + align-items: center; + justify-content: center; +} + +.tshirt-ad__dismiss:hover { + background: #fff; + color: #1a202c; +} + +.tshirt-ad__label { + position: absolute; + bottom: 4px; + left: 4px; + font-size: 10px; + color: #718096; + background: rgba(255, 255, 255, 0.85); + padding: 1px 4px; + border-radius: 2px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +@media (max-width: 1279px) { + .tshirt-ad--left, + .tshirt-ad--right { + display: none; + } +} + +/* Show the wide 728x90 banner only on tablet+ viewports that can fit it */ +@media (max-width: 767px) { + .tshirt-ad--bottom { + display: none; + } +} + +/* Show the 320x50 mobile banner only on phone-width viewports */ +@media (min-width: 768px) { + .tshirt-ad--bottom-mobile { + display: none; + } +} diff --git a/src/styles/layout.css b/src/styles/layout.css index 8d7f28b..80308e5 100644 --- a/src/styles/layout.css +++ b/src/styles/layout.css @@ -172,3 +172,14 @@ footer li { overflow-x: auto; } } + +/* When 160x600 side rails are visible (viewport >= 1280px), reserve 200px on + each side so .content-section and footer don't slide under the ads. The + ad occupies 0-176px from the viewport edge; 200px gives 24px of breathing + room. Capped at 80% so very wide screens still look balanced. */ +@media (min-width: 1280px) { + .content-section, + footer { + max-width: min(80%, calc(100% - 400px)); + } +}