Diff -- /var/www/vhosts/3dshawn.com/abforge.3dshawn.com/index.cgi
Diff

/var/www/vhosts/3dshawn.com/abforge.3dshawn.com/index.cgi

added on local at 2026-07-01 16:01:23

Added
+268
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 02691c1fd682
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2#======================================================================
3# ABForge - Run your 3D creator business like an operator
4# Page body lives in TEMPLATES/abforge_index.html
5#
6# Pricing block on this page pulls from billing_plans so it stays in
7# lockstep with /pricing.cgi and the admin Packages console. Falls
8# back to a built-in 4-tier list if the billing schema hasn't been
9# migrated yet.
10#
11# Dispatches marketing sub-pages via SCRIPT_NAME:
12# /index.cgi -- landing page (this file)
13# /about.cgi -- static: abforge_about.html
14# /contact.cgi -- static: abforge_contact.html
15# /faq.cgi -- static: abforge_faq.html
16# /features.cgi -- static: abforge_features.html
17# /help.cgi -- static: abforge_help.html (login-gated)
18# /privacy.cgi -- static: abforge_privacy.html
19# /terms.cgi -- static: abforge_terms.html
20# /why.cgi -- static: abforge_why.html
21#======================================================================
22use strict;
23use warnings;
24
25use lib '/var/www/vhosts/abforge.com/httpdocs';
26use CGI;
27use MODS::Template;
28use MODS::DBConnect;
29use MODS::Login;
30use MODS::ABForge::Config;
31use MODS::ABForge::Wrapper;
32use MODS::ABForge::Billing;
33use MODS::ABForge::Promotions;
34
35my $q = CGI->new;
36my $form = $q->Vars;
37my $auth = MODS::Login->new;
38my $wrap = MODS::ABForge::Wrapper->new;
39my $tfile = MODS::Template->new;
40my $db = MODS::DBConnect->new;
41my $cfg = MODS::ABForge::Config->new;
42my $bill = MODS::ABForge::Billing->new;
43my $DB = $cfg->settings('database_name');
44
45$|=1;
46my $userinfo = $auth->login_verify(); # may be undef on marketing pages
47
48my $entry = ($ENV{SCRIPT_NAME} || '') =~ m{/([^/]+)\.cgi$} ? $1 : 'index';
49
50# Simple static marketing pages. Each renders one abforge_<name>.html
51# template inside the standard site wrapper.
52my %_mkt_static = (
53 about => { template => 'abforge_about.html', title => 'About' },
54 contact => { template => 'abforge_contact.html', title => 'Contact' },
55 faq => { template => 'abforge_faq.html', title => 'FAQ' },
56 features => { template => 'abforge_features.html', title => 'Features' },
57 privacy => { template => 'abforge_privacy.html', title => 'Privacy Policy' },
58 terms => { template => 'abforge_terms.html', title => 'Terms of Service' },
59 why => { template => 'abforge_why.html', title => 'Why ABForge' },
60);
61
62if ($_mkt_static{$entry}) {
63 _mkt_render_static($_mkt_static{$entry});
64 exit;
65}
66
67if ($entry eq 'help') {
68 # help.cgi requires login (unlike the other marketing pages).
69 if (!$userinfo) {
70 print "Status: 302 Found\nLocation: /login.cgi\n\n";
71 exit;
72 }
73 print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
74 my $body = join('', $tfile->template('abforge_help.html', {}, $userinfo));
75 $wrap->render({
76 userinfo => $userinfo,
77 page_key => '',
78 title => 'Help',
79 body => $body,
80 });
81 exit;
82}
83
84# ================ index.cgi landing page (default) ==================
85my $dbh = $db->db_connect();
86my @plans = _build_landing_plan_cards($db, $dbh, $DB, $bill);
87
88# Active plan promo for the marketing banner above the pricing block.
89my $promo_obj = MODS::ABForge::Promotions->new;
90my $active_promo = $promo_obj->active_plan_promo($db, $dbh, $DB);
91my $promo_code = ($active_promo && $active_promo->{code}) ? $active_promo->{code} : '';
92my $promo_label = $active_promo ? $promo_obj->marketing_label($active_promo) : '';
93
94# Per-card eligibility badge (same logic as /pricing.cgi). Cards the
95# active promo doesn't cover (free + plans outside the eligibility
96# set) simply don't render the promo line.
97if ($active_promo) {
98 my @eligible = $promo_obj->eligible_plan_ids($db, $dbh, $DB, $active_promo->{id});
99 my %eligible_set = map { $_ => 1 } @eligible;
100 my $all_paid = !scalar(@eligible);
101 foreach my $c (@plans) {
102 next if ($c->{price_display} || '$0') eq '$0';
103 next unless $all_paid || $eligible_set{ $c->{id} || 0 };
104 $c->{promo_active} = 1;
105 $c->{promo_code} = $promo_code;
106 $c->{promo_label} = $promo_label;
107 $c->{promo_card_line} = $promo_label . ' with code <strong>' . $promo_code . '</strong>';
108 }
109}
110
111$db->db_disconnect($dbh);
112
113print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
114
115my $tvars = {
116 user_id => $userinfo ? $userinfo->{user_id} : '',
117 plans => \@plans,
118 has_plans => scalar(@plans) ? 1 : 0,
119 has_promo => $active_promo ? 1 : 0,
120 promo_code => $promo_code,
121 promo_label => $promo_label,
122};
123
124my $body = join('', $tfile->template('abforge_index.html', $tvars, $userinfo));
125
126$wrap->render({
127 userinfo => $userinfo,
128 title => 'Close more sales with the traffic you already have',
129 body => $body,
130});
131
132exit;
133
134#----------------------------------------------------------------------
135# Render one of the small static marketing pages.
136sub _mkt_render_static {
137 my ($spec) = @_;
138 print "Content-Type: text/html; charset=utf-8\nCache-Control: no-cache\n\n";
139 my $body = join('', $tfile->template($spec->{template}, {}, $userinfo));
140 $wrap->render({
141 userinfo => $userinfo,
142 title => $spec->{title},
143 body => $body,
144 });
145}
146
147#----------------------------------------------------------------------
148# Compact card shape used by the landing pricing grid (4 cards, one
149# per tier, monthly price). Bullets are pre-rendered to side-step the
150# template engine's nested-loop quirks.
151sub _build_landing_plan_cards {
152 my ($db, $dbh, $DB, $bill) = @_;
153 return _landing_fallback_plans() unless $bill->schema_ready($db, $dbh, $DB);
154
155 my @rows = $bill->list_plans($db, $dbh, $DB);
156 return _landing_fallback_plans() unless scalar @rows;
157
158 my $entitle_ready = $bill->plan_features_ready($db, $dbh, $DB);
159 my @catalog = $bill->feature_catalog;
160
161 my %by_tier;
162 foreach my $r (@rows) { push @{ $by_tier{ $r->{tier} } }, $r; }
163
164 my @tiers_seen;
165 foreach my $r (@rows) {
166 next if grep { $_ eq $r->{tier} } @tiers_seen;
167 push @tiers_seen, $r->{tier};
168 }
169
170 my @out;
171 foreach my $tier (@tiers_seen) {
172 my $monthly = (grep { ($_->{cadence}||'') eq 'monthly' } @{ $by_tier{$tier} })[0]
173 || $by_tier{$tier}->[0];
174 next unless $monthly;
175
176 # Build a short bullet list. Prefer the marketing copy supplied
177 # by the admin; otherwise synthesise from entitlements so the
178 # card never renders empty on a fresh install.
179 my @bullets = _parse_features_for_landing($monthly->{features});
180 if (!scalar @bullets && $entitle_ready) {
181 my $set = $bill->plan_feature_set($db, $dbh, $DB, $monthly->{id});
182 foreach my $f (@catalog) {
183 next unless $set->{ $f->{key} };
184 push @bullets, $f->{name} . ' &mdash; ' . $f->{blurb};
185 }
186 }
187 # Cap to 5 bullets to match the original card visual density.
188 @bullets = @bullets[0 .. ( $#bullets > 4 ? 4 : $#bullets )];
189 my $bullets_html = '';
190 foreach my $b (@bullets) {
191 $bullets_html .= '<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">'
192 . '<polyline points="20 6 9 17 4 12"/></svg> ' . $b . '</li>';
193 }
194
195 my $tier_slug = lc($monthly->{tier} || ''); $tier_slug =~ s/[^a-z0-9_\-]//g;
196
197 push @out, {
198 id => $monthly->{id},
199 tier => $monthly->{tier},
200 display_name => $monthly->{display_name} || ucfirst($monthly->{tier} || ''),
201 price_display => _fmt_price_short($monthly->{price_cents}),
202 is_featured => $monthly->{is_featured} ? 1 : 0,
203 not_featured => $monthly->{is_featured} ? 0 : 1,
204 tagline => $monthly->{tagline} || '',
205 has_tagline => $monthly->{tagline} ? 1 : 0,
206 cta_label => $monthly->{cta_label} || ($monthly->{price_cents} ? 'Start trial' : 'Start free'),
207 cta_href => $monthly->{price_cents} ? ('signup.cgi?plan=' . $tier_slug) : 'signup.cgi',
208 bullets_html => $bullets_html,
209 };
210 }
211 return @out;
212}
213
214sub _fmt_price_short {
215 my ($cents) = @_;
216 $cents = 0 unless defined $cents;
217 if ($cents % 100 == 0) {
218 return '$' . int($cents / 100);
219 }
220 return '$' . sprintf('%.2f', $cents / 100);
221}
222
223sub _parse_features_for_landing {
224 my ($raw) = @_;
225 return () unless defined $raw && $raw =~ /\S/;
226 my @out;
227 while ($raw =~ m/\{\s*"name"\s*:\s*"((?:\\.|[^"\\])*)"\s*,\s*"included"\s*:\s*(true|false|0|1)\s*\}/gi) {
228 my $name = $1; my $inc = lc $2;
229 $name =~ s/\\"/"/g; $name =~ s/\\\\/\\/g;
230 # Landing block only shows the positive bullets.
231 push @out, $name if $inc eq 'true' || $inc eq '1';
232 }
233 return @out;
234}
235
236sub _landing_fallback_plans {
237 my $li = sub {
238 my @items = @_;
239 my $h = '';
240 foreach my $t (@items) {
241 $h .= '<li><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3">'
242 . '<polyline points="20 6 9 17 4 12"/></svg> ' . $t . '</li>';
243 }
244 return $h;
245 };
246 return (
247 { tier=>'free', display_name=>'Free', price_display=>'$0',
248 is_featured=>0, not_featured=>1,
249 tagline=>'Solo creators just publishing to one or two marketplaces.',
250 has_tagline=>1, cta_label=>'Start free', cta_href=>'signup.cgi',
251 bullets_html => $li->('Up to 10 models', '2 platform connections', 'Cross-platform analytics') },
252 { tier=>'starter', display_name=>'Starter', price_display=>'$9',
253 is_featured=>0, not_featured=>1,
254 tagline=>'Active creators publishing to several marketplaces.',
255 has_tagline=>1, cta_label=>'Start trial', cta_href=>'signup.cgi?plan=starter',
256 bullets_html => $li->('Unlimited models', 'All platform connections', 'Per-platform overrides', 'Priority publishing queue') },
257 { tier=>'pro', display_name=>'Pro', price_display=>'$29',
258 is_featured=>1, not_featured=>0,
259 tagline=>'Storefront + optimization for creators going independent.',
260 has_tagline=>1, cta_label=>'Start trial', cta_href=>'signup.cgi?plan=pro',
261 bullets_html => $li->('Everything in Starter', 'Branded storefront + custom domain', 'A/B + MVT testing', 'Price discovery tools', 'Stripe Connect direct payouts') },
262 { tier=>'studio', display_name=>'Studio', price_display=>'$99',
263 is_featured=>0, not_featured=>1,
264 tagline=>'Teams. Multiple storefronts. Pooled-data recommendations.',
265 has_tagline=>1, cta_label=>'Start trial', cta_href=>'signup.cgi?plan=studio',
266 bullets_html => $li->('Everything in Pro', 'Up to 10 team members + roles', 'Multiple storefronts', 'Pooled-data recommendations', '2FA mandatory + audit log access') },
267 );
268}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help