| 1 | import { ImageResponse } from 'next/og' |
| 2 | import type { NextRequest |
| 3 | import { SaturnLogoLight } |
| 4 | import { baseUrl } |
| 5 | |
| 6 | export const runtime = |
| 7 | |
| 8 | const size = { |
| 9 | |
| 10 | const subtitles: Record |
| 11 | Invoices: 'Create, send, and track invoices', |
| 12 | Clients: 'Manage client relationships and access', |
| 13 | Milestones: 'Track project milestones and deliverables', |
| 14 | Requirements: 'Write, review, and sign off on requirements', |
| 15 | Timesheets: 'Log hours and track team productivity', |
| 16 | Expenses: 'Track and approve project expenses', |
| 17 | Team: 'Manage your team members and roles', |
| 18 | Settings: 'Configure your project and organization', |
| 19 | Overview: 'Your project at a glance', |
| 20 | Projects: 'Manage projects, requirements, and invoices', |
| 21 | Proposals: 'Draft, send, and get proposals signed', |
| 22 | Time: 'Track time against the right project', |
| 23 | } |
| 24 | |
| 25 | const moduleRail = [ |
| 26 | 'Projects', |
| 27 | 'Clients', |
| 28 | 'Proposals', |
| 29 | 'Requirements', |
| 30 | 'Timesheets', |
| 31 | 'Expenses', |
| 32 | 'Invoices', |
| 33 | 'Milestones', |
| 34 | ] |
| 35 | |
| 36 | async function loadFonts() |
| 37 | const [bold, |
| 38 | fetch( |
| 39 | new URL( |
| 40 | 'https://cdn.jsdelivr.net/fontsource/fonts/geist-sans@latest/latin-600-normal.woff' |
| 41 | ) |
| 42 | ).then((r |
| 43 | fetch( |
| 44 | new URL( |
| 45 | 'https://cdn.jsdelivr.net/fontsource/fonts/geist-sans@latest/latin-400-normal.woff' |
| 46 | ) |
| 47 | ).then((r |
| 48 | ]) |
| 49 | return { bold, |
| 50 | } |
| 51 | |
| 52 | export async function GET |
| 53 | const page = |
| 54 | const subtitleParam = |
| 55 | |
| 56 | const isMarketing = |
| 57 | const headlineLead = |
| 58 | const headlineAccent = |
| 59 | const subtitle = |
| 60 | subtitleParam ?? |
| 61 | (isMarketing |
| 62 | ? 'Projects, proposals, time, timesheets, expenses, and invoices — one login instead of seven subscriptions.' |
| 63 | : (page && |
| 64 | |
| 65 | const eyebrow = |
| 66 | ? 'For freelancers, agencies & solo builders' |
| 67 | : `/ ${page} |
| 68 | |
| 69 | const activeIndex = |
| 70 | ? moduleRail.findIndex( |
| 71 | : -1 |
| 72 | const domain = |
| 73 | |
| 74 | const { bold, |
| 75 | |
| 76 | return new ImageResponse |
| 77 | <div |
| 78 | style={{ background: |
| 79 | tw='flex w-full h-full relative' |
| 80 | > |
| 81 | {/* grid */} |
| 82 | <div |
| 83 | style={{ |
| 84 | inset: 0, |
| 85 | backgroundImage: |
| 86 | 'linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px)', |
| 87 | backgroundSize: '72px 72px', |
| 88 | }} |
| 89 | tw='absolute flex' |
| 90 | /> |
| 91 | |
| 92 | {/* purple glow — anchored to the seam between the two columns */ |
| 93 | <div |
| 94 | style={{ |
| 95 | left: '52%', |
| 96 | top: '50%', |
| 97 | transform: 'translate(-50%, -50%)', |
| 98 | width: 760, |
| 99 | height: 760, |
| 100 | borderRadius: '50%', |
| 101 | background: |
| 102 | 'radial-gradient(circle, rgba(124,58,237,0.32) 0%, rgba(124,58,237,0.08) 40%, transparent 70%)', |
| 103 | filter: 'blur(20px)', |
| 104 | }} |
| 105 | tw='absolute flex' |
| 106 | /> |
| 107 | |
| 108 | {/* hairlines */} |
| 109 | <div |
| 110 | style={{ |
| 111 | left: 0, |
| 112 | right: 0, |
| 113 | top: 0, |
| 114 | height: 1, |
| 115 | background: |
| 116 | 'linear-gradient(to right, transparent, rgba(255,255,255,0.14), transparent)', |
| 117 | }} |
| 118 | tw='absolute flex' |
| 119 | /> |
| 120 | <div |
| 121 | style={{ |
| 122 | left: 0, |
| 123 | right: 0, |
| 124 | bottom: 0, |
| 125 | height: 1, |
| 126 | background: |
| 127 | 'linear-gradient(to right, transparent, rgba(255,255,255,0.14), transparent)', |
| 128 | }} |
| 129 | tw='absolute flex' |
| 130 | /> |
| 131 | |
| 132 | {/* header rail */} |
| 133 | <div |
| 134 | style={{ |
| 135 | left: 64, |
| 136 | right: 64, |
| 137 | top: 44, |
| 138 | }} |
| 139 | tw='absolute flex items-center justify-between' |
| 140 | > |
| 141 | <div style={{ |
| 142 | <SaturnLogoLight height={ |
| 143 | <span |
| 144 | style={{ letterSpacing: |
| 145 | tw='text-[20px] font-semibold text-white' |
| 146 | > |
| 147 | Saturn |
| 148 | </span> |
| 149 | </div> |
| 150 | <div |
| 151 | style={{ |
| 152 | letterSpacing: '2.4px', |
| 153 | color: 'rgba(255,255,255,0.4)', |
| 154 | }} |
| 155 | tw='flex text-[12px] uppercase' |
| 156 | > |
| 157 | {domain} |
| 158 | </div> |
| 159 | </div> |
| 160 | |
| 161 | {/* left: text column */} |
| 162 | <div |
| 163 | style={{ padding: |
| 164 | tw='flex flex-col justify-center h-full relative' |
| 165 | > |
| 166 | <div |
| 167 | style={{ |
| 168 | letterSpacing: '2.6px', |
| 169 | color: 'rgba(255,255,255,0.5)', |
| 170 | }} |
| 171 | tw='flex items-center text-[13px] uppercase' |
| 172 | > |
| 173 | <span |
| 174 | style={{ |
| 175 | width: 28, |
| 176 | height: 1, |
| 177 | background: 'rgba(255,255,255,0.3)', |
| 178 | marginRight: 14, |
| 179 | }} |
| 180 | tw='flex' |
| 181 | /> |
| 182 | {eyebrow} |
| 183 | </div> |
| 184 | |
| 185 | <div |
| 186 | style={{ |
| 187 | lineHeight: 0.96, |
| 188 | letterSpacing: '-3.4px', |
| 189 | }} |
| 190 | tw='flex flex-col text-[80px] font-semibold text-white' |
| 191 | > |
| 192 | <span>{headlineLead}</ |
| 193 | {headlineAccent && ( |
| 194 | <span |
| 195 | style={{ |
| 196 | color: 'rgba(255,255,255,0.42)', |
| 197 | fontStyle: 'italic', |
| 198 | }} |
| 199 | > |
| 200 | {headlineAccent} |
| 201 | </span> |
| 202 | )} |
| 203 | </div> |
| 204 | |
| 205 | <p |
| 206 | style={{ |
| 207 | letterSpacing: '-0.3px', |
| 208 | color: 'rgba(255,255,255,0.55)', |
| 209 | margin: 0, |
| 210 | lineHeight: 1.4, |
| 211 | maxWidth: 560, |
| 212 | }} |
| 213 | tw='text-[21px]' |
| 214 | > |
| 215 | {subtitle} |
| 216 | </p> |
| 217 | </div> |
| 218 | |
| 219 | {/* right: oversized module rail */ |
| 220 | <div |
| 221 | style={{ |
| 222 | right: 0, |
| 223 | top: 0, |
| 224 | bottom: 0, |
| 225 | width: 420, |
| 226 | paddingRight: 64, |
| 227 | paddingTop: 110, |
| 228 | paddingBottom: 90, |
| 229 | borderLeft: '1px solid rgba(255,255,255,0.06)', |
| 230 | gap: 6, |
| 231 | }} |
| 232 | tw='absolute flex flex-col justify-center items-end' |
| 233 | > |
| 234 | <div |
| 235 | style={{ |
| 236 | letterSpacing: '2.4px', |
| 237 | color: 'rgba(255,255,255,0.35)', |
| 238 | marginBottom: 14, |
| 239 | }} |
| 240 | tw='flex text-[11px] uppercase' |
| 241 | > |
| 242 | / one workspace |
| 243 | </div> |
| 244 | {moduleRail.map(( |
| 245 | const active = |
| 246 | const distance = |
| 247 | return ( |
| 248 | <div |
| 249 | key={m} |
| 250 | style={{ |
| 251 | gap: 14, |
| 252 | color: active |
| 253 | ? 'rgba(255,255,255,0.95)' |
| 254 | : 'rgba(255,255,255,1)', |
| 255 | opacity: |
| 256 | activeIndex < 0 |
| 257 | ? 0.32 |
| 258 | : active |
| 259 | ? 1 |
| 260 | : Math.max( |
| 261 | letterSpacing: '-1.4px', |
| 262 | lineHeight: 1.05, |
| 263 | }} |
| 264 | tw='flex items-center text-[40px] font-semibold' |
| 265 | > |
| 266 | {active && ( |
| 267 | <span |
| 268 | style={{ |
| 269 | width: 10, |
| 270 | height: 10, |
| 271 | borderRadius: 999, |
| 272 | background: 'rgba(167,139,250,1)', |
| 273 | boxShadow: '0 0 18px rgba(167,139,250,0.8)', |
| 274 | }} |
| 275 | tw='flex' |
| 276 | /> |
| 277 | )} |
| 278 | <span style={active |
| 279 | {m} |
| 280 | </span> |
| 281 | </div> |
| 282 | ) |
| 283 | })} |
| 284 | </div> |
| 285 | |
| 286 | {/* fade behind module rail to blend into glow */ |
| 287 | <div |
| 288 | style={{ |
| 289 | right: 0, |
| 290 | top: 0, |
| 291 | bottom: 0, |
| 292 | width: 420, |
| 293 | background: |
| 294 | 'linear-gradient(to right, transparent, rgba(10,10,10,0.55))', |
| 295 | pointerEvents: 'none', |
| 296 | }} |
| 297 | tw='absolute flex' |
| 298 | /> |
| 299 | |
| 300 | {/* footer eyebrow */} |
| 301 | <div |
| 302 | style={{ |
| 303 | left: 64, |
| 304 | right: 64, |
| 305 | bottom: 44, |
| 306 | }} |
| 307 | tw='absolute flex items-center' |
| 308 | > |
| 309 | <div |
| 310 | style={{ |
| 311 | letterSpacing: '2.4px', |
| 312 | color: 'rgba(255,255,255,0.4)', |
| 313 | }} |
| 314 | tw='flex text-[12px] uppercase' |
| 315 | > |
| 316 | {isMarketing ? 'Projects · Time · Invoices · Done. |
| 317 | </div> |
| 318 | </div> |
| 319 | </div>, |
| 320 | { |
| 321 | ...size, |
| 322 | fonts: [ |
| 323 | { |
| 324 | name: 'Geist', |
| 325 | data: bold, |
| 326 | style: 'normal' as |
| 327 | weight: 600 as const |
| 328 | }, |
| 329 | { |
| 330 | name: 'Geist', |
| 331 | data: regular, |
| 332 | style: 'normal' as |
| 333 | weight: 400 as const |
| 334 | }, |
| 335 | ], |
| 336 | } |
| 337 | ) |
| 338 | } |