-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild_site.py
More file actions
268 lines (232 loc) · 12.9 KB
/
Copy pathbuild_site.py
File metadata and controls
268 lines (232 loc) · 12.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
#!/usr/bin/env python3
"""
REI TOOLKIT — full site generator (v2).
Builds all 6 pages (hub + 5 calculators) with RELATIVE paths so the site
works from any host/subpath. New modern design system in assets/site.css.
Calculator math JS files are reused unchanged.
ROOT prefix convention:
hub (public/index.html) -> ROOT = "" (assets/..., brrrr/...)
tools (public/<tool>/index.html) -> ROOT = "../" (../assets/..., ../brrrr/...)
"""
import os
OUT = os.path.join(os.path.dirname(__file__), "public")
TOOLS = [
("brrrr", "BRRRR", "BRRRR Calculator", "Buy \u00b7 Rehab \u00b7 Rent \u00b7 Refinance \u00b7 Repeat"),
("rental", "Cash Flow", "Rental Cash Flow", "Buy-and-hold cash flow & cap rate"),
("flip", "Fix & Flip", "Fix & Flip Calculator", "70% rule, profit & ROI on flips"),
("mortgage", "Mortgage", "Mortgage & Refi", "Payment, amortization & refi breakeven"),
("rehab", "Rehab", "Rehab Cost Estimator", "Room-by-room renovation budget"),
]
# ---- shared SVG icons ----
LOGO = ('<svg class="header-logo" viewBox="0 0 40 40" fill="none" aria-hidden="true">'
'<rect x="1" y="1" width="38" height="38" rx="11" fill="currentColor" opacity="0.12"/>'
'<path d="M9 22 L20 12 L31 22" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>'
'<path d="M12 20.5 V30 H28 V20.5" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>'
'<path d="M17 30 V24 H23 V30" stroke="currentColor" stroke-width="2.2" stroke-linejoin="round"/></svg>')
FOOTER_LOGO = ('<svg viewBox="0 0 40 40" fill="none" aria-hidden="true">'
'<rect x="1" y="1" width="38" height="38" rx="11" fill="currentColor" opacity="0.14"/>'
'<path d="M9 22 L20 12 L31 22" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/>'
'<path d="M12 20.5 V30 H28 V20.5" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg>')
ICONS = {
"tag": '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20.59 13.41 13.42 20.6a2 2 0 0 1-2.83 0L2 12V2h10l8.59 8.59a2 2 0 0 1 0 2.82z"/><circle cx="7" cy="7" r="1.2"/></svg>',
"chart": '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 3v18h18"/><path d="M7 14l3-4 3 3 5-7"/></svg>',
"home": '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M3 10.5 12 3l9 7.5"/><path d="M5 9.5V21h14V9.5"/></svg>',
"money": '<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><path d="M14.5 9a2.5 2.5 0 0 0-2.5-1.5C10.5 7.5 9.5 8.4 9.5 9.6c0 2.6 5 1.4 5 4 0 1.2-1 2.1-2.5 2.1A2.7 2.7 0 0 1 9.5 14"/><path d="M12 6v1.5M12 16v1.5"/></svg>',
"wrench":'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14.7 6.3a4 4 0 0 0-5.4 5.3L3 18v3h3l6.4-6.3a4 4 0 0 0 5.3-5.4l-2.6 2.6-2.1-2.1z"/></svg>',
"refresh":'<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12a9 9 0 1 1-2.6-6.4"/><path d="M21 3v5h-5"/></svg>',
"book": '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 4.5A2.5 2.5 0 0 1 6.5 2H20v18H6.5A2.5 2.5 0 0 0 4 22.5z"/><path d="M4 4.5V22"/></svg>',
}
TOOL_ICON = {"brrrr": "home", "rental": "chart", "flip": "money", "mortgage": "tag", "rehab": "wrench"}
def head(title, desc, root):
return f'''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<meta name="description" content="{desc}" />
<link rel="preconnect" href="https://api.fontshare.com" crossorigin />
<link href="https://api.fontshare.com/v2/css?f[]=clash-display@500,600,700&f[]=satoshi@400,500,600,700&display=swap" rel="stylesheet" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="{root}assets/site.css" />
<!-- Google Tag Manager (manage your GA4 tag inside GTM container GTM-P9RGF3Z9) -->
<script>(function(w,d,s,l,i){{w[l]=w[l]||[];w[l].push({{'gtm.start':new Date().getTime(),event:'gtm.js'}});var f=d.getElementsByTagName(s)[0],j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src='https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);}})(window,document,'script','dataLayer','GTM-P9RGF3Z9');</script>
<!-- End Google Tag Manager -->
</head>
<body>
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-P9RGF3Z9" height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->'''
def header(active, subtitle, root):
nav = "\n".join(
f' <a href="{root}{k}/"{" aria-current=\"page\"" if k == active else ""}>{label.replace("&", "&")}</a>'
for k, label, _, _ in TOOLS)
return f''' <header class="header">
<a class="header-brand" href="{root}" aria-label="REI Toolkit home">
{LOGO}
<div>
<div class="header-title">REI Toolkit</div>
<div class="header-subtitle">{subtitle}</div>
</div>
</a>
<div class="header-actions">
<nav class="site-nav" aria-label="Calculators">
{nav}
</nav>
<button class="theme-toggle" data-theme-toggle aria-label="Toggle dark / light mode" title="Toggle theme"></button>
<button class="nav-toggle" data-nav-toggle aria-label="Open menu" aria-expanded="false" aria-controls="mobile-nav">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/></svg>
</button>
</div>
</header>'''
def mobile_nav(active, root):
links = "\n".join(
f' <a href="{root}{k}/"{" aria-current=\"page\"" if k == active else ""}>{full}</a>'
for k, _, full, _ in TOOLS)
home = f' <a href="{root}"{" aria-current=\"page\"" if active is None else ""}>Home</a>'
return f''' <nav class="mobile-nav" id="mobile-nav" aria-label="Calculators">
{home}
{links}
</nav>'''
def more_tools(active, root, heading="Keep analyzing"):
cards = []
for k, _, full, tag in TOOLS:
if k == active:
continue
ic = ICONS[TOOL_ICON[k]]
cards.append(
f''' <a class="tool-card" href="{root}{k}/" data-analytics-event="More Tool Click">
<span class="tool-card-icon">{ic}</span>
<div class="tool-card-title">{full}</div>
<p class="tool-card-desc">{tag}</p>
<span class="tool-card-cta">Open tool</span>
</a>''')
cards = "\n".join(cards[:3])
return f''' <section class="section-tight">
<div class="content-wrapper">
<span class="section-eyebrow">More tools</span>
<h2 class="section-lead-title">{heading}</h2>
<div class="tool-grid mt-6">
{cards}
</div>
</div>
</section>'''
def footer(root):
links = "\n".join(
f' <a href="{root}{k}/">{full}</a>' for k, _, full, _ in TOOLS)
return f''' <footer class="site-footer">
<div class="site-footer-inner">
<div class="footer-grid">
<div>
<div class="site-footer-brand">{FOOTER_LOGO} REI Toolkit</div>
<p class="site-footer-disclaimer">Free calculators for real estate investors. Estimates only \u2014 not financial, legal, or tax advice. Always confirm figures with your lender, accountant, and contractor. Some outbound links may be affiliate or sponsored.</p>
</div>
<div class="footer-links">
<span class="footer-links-title">Calculators</span>
{links}
</div>
</div>
</div>
</footer>'''
def scripts(root, page_js):
return f''' <script src="{root}assets/site.js" defer></script>
<script src="{page_js}" defer></script>
</body>
</html>'''
# ---------- field + result helpers ----------
def field(fid, label, hint, default, prefix="$", suffix=None, full=False, err=True, step=None):
cls = "field full" if full else "field"
wrap_cls = "input-wrap"
pre = suf = ""
if prefix:
wrap_cls += " has-prefix"
pre = f'<span class="input-prefix">{prefix}</span>'
if suffix:
wrap_cls += " has-suffix"
suf = f'<span class="input-suffix">{suffix}</span>'
step_attr = f' step="{step}"' if step else ""
err_html = f'\n <span class="field-error" id="{fid}-error"></span>' if err else ""
hint_html = f'\n <span class="field-hint">{hint}</span>' if hint else ""
return f''' <div class="{cls}">
<label for="{fid}">{label}</label>
<div class="{wrap_cls}">{pre}<input type="number" id="{fid}" value="{default}" inputmode="decimal"{step_attr} />{suf}</div>{hint_html}{err_html}
</div>'''
def rrow(label, rid, sub=False):
cls = "result-row subhead" if sub else "result-row"
return f''' <div class="{cls}"><span class="result-label">{label}</span><span class="result-value" id="{rid}">\u2014</span></div>'''
def resources_box(sub, links, root):
li = "\n".join(
f' <a href="{href}" rel="{rel}" target="_blank" data-analytics-event="{ev}">{text}</a>'
for text, href, rel, ev in links)
return f''' <aside class="resources">
<div class="resources-title">{ICONS["book"]} Resources & tools</div>
<p class="resources-sub">{sub}</p>
<div class="resources-list">
{li}
</div>
<p class="resources-disclaimer">Estimates only \u2014 not financial advice. Some links may be affiliate or sponsored. Verify all figures with your lender, accountant, and contractor.</p>
</aside>'''
def hero(eyebrow, title, lede, stats):
stat_html = "\n".join(
f''' <div><div class="hero-stat-num">{n}</div><div class="hero-stat-label">{l}</div></div>'''
for n, l in stats)
return f''' <section class="hero">
<div class="content-wrapper">
<div class="hero-card">
<div class="hero-inner">
<span class="section-eyebrow">{eyebrow}</span>
<h1>{title}</h1>
<p class="hero-lede">{lede}</p>
<div class="hero-stats">
{stat_html}
</div>
</div>
</div>
</div>
</section>'''
# ---------- ad slot helpers (placeholder containers) ----------
def ad_leaderboard():
"""Full-width responsive banner (e.g. 728x90 / fluid). Placeholder."""
return ''' <div class="ad-leaderboard-wrap">
<div class="ad-slot ad-leaderboard" data-ad-slot="leaderboard" aria-label="Advertisement">
<span class="ad-label">Advertisement</span>
<!-- Drop your ad code (e.g. AdSense <ins>) here. See DEVELOPMENT.md \u00a7 Ads. -->
</div>
</div>'''
def ad_rectangle():
"""Medium rectangle (300x250) for calculator sidebar. Placeholder."""
return ''' <div class="ad-slot ad-rectangle" data-ad-slot="sidebar-rectangle" aria-label="Advertisement">
<span class="ad-label">Advertisement</span>
<!-- Drop your ad code (e.g. AdSense <ins>) here. See DEVELOPMENT.md \u00a7 Ads. -->
</div>'''
def ad_inline():
"""In-content banner between sections. Placeholder."""
return ''' <section class="section-tight">
<div class="content-wrapper">
<div class="ad-slot ad-inline" data-ad-slot="in-content" aria-label="Advertisement">
<span class="ad-label">Advertisement</span>
<!-- Drop your ad code (e.g. AdSense <ins>) here. See DEVELOPMENT.md \u00a7 Ads. -->
</div>
</div>
</section>'''
def formula_section(cards):
c = "\n".join(
f''' <div class="formula-card">
<div class="formula-step">{step}</div>
<code class="formula-eq">{eq}</code>
<p class="formula-desc">{desc}</p>
</div>''' for step, eq, desc in cards)
return f''' <section class="section-tight">
<div class="content-wrapper">
<span class="section-eyebrow">Transparent math</span>
<h2 class="section-lead-title">How the formulas work</h2>
<div class="formula-grid">
{c}
</div>
</div>
</section>'''
# tool-specific page bodies are defined in build_pages.py (imported)
if __name__ == "__main__":
print("module ready")