feat: landing page CRO revision, demo page, auto-trial, multi-catalog messaging

- Rewrite landing page copy with Jobs-to-be-Done and loss framing
- Add VIN live preview via backend pl24+emex decode chain
- Add comparison table (mobile-responsive), stats, pricing teaser, referral banner
- Replace testimonials with aggregate social proof stats
- Create demo page with 3-step VIN decode flow for unregistered users
- Add public /vehicles/preview/:vin endpoint (no auth required)
- Auto-create 7-day trial subscription on user registration
- Highlight multi-catalog cross-querying advantage in OEM feature and comparison
- Update register page with trial messaging and VIN param support

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sase Dev
2026-02-15 00:38:52 +00:00
parent 4a06ba5fdb
commit 25278d386a
13 changed files with 1800 additions and 72 deletions

View File

@@ -1,6 +1,7 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { drizzle } from "drizzle-orm/postgres-js";
import { eq, asc } from "drizzle-orm";
import postgres from "postgres";
import * as schema from "../database/schema/core";
@@ -56,6 +57,40 @@ export function createAuth(databaseUrl: string, secret: string, baseUrl: string)
},
},
},
databaseHooks: {
user: {
create: {
after: async (user) => {
try {
// Find the lowest-tier plan for trial
const [plan] = await db
.select()
.from(schema.plans)
.where(eq(schema.plans.isActive, true))
.orderBy(asc(schema.plans.priceMonthly))
.limit(1);
if (!plan) return;
const now = new Date();
const endDate = new Date(now);
endDate.setDate(endDate.getDate() + 7);
await db.insert(schema.userSubscriptions).values({
userId: user.id,
planId: plan.id,
status: "trial",
billingPeriod: "monthly",
startDate: now,
endDate,
});
} catch (err) {
console.error("Failed to create trial subscription:", err);
}
},
},
},
},
trustedOrigins: [
...(process.env.CORS_ORIGIN || "http://localhost:3000").split(","),
"http://localhost:4000",

View File

@@ -5,7 +5,7 @@ import {
Injectable,
NotFoundException,
} from "@nestjs/common";
import { eq, and, desc } from "drizzle-orm";
import { eq, and, desc, or, inArray } from "drizzle-orm";
import { DATABASE, Database } from "../database/database.provider";
import { userSubscriptions, userBrands, plans, brands } from "../database/schema/core";
@@ -25,6 +25,12 @@ export class SubscriptionsService {
throw new ConflictException("Already have an active subscription");
}
// Expire any existing trial subscription
await this.db
.update(userSubscriptions)
.set({ status: "expired", updatedAt: new Date() })
.where(and(eq(userSubscriptions.userId, userId), eq(userSubscriptions.status, "trial")));
// Validate plan
const plan = await this.db.select().from(plans).where(eq(plans.id, data.planId)).limit(1);
if (plan.length === 0) throw new NotFoundException("Plan not found");
@@ -198,7 +204,12 @@ export class SubscriptionsService {
const [sub] = await this.db
.select()
.from(userSubscriptions)
.where(and(eq(userSubscriptions.userId, userId), eq(userSubscriptions.status, "active")))
.where(
and(
eq(userSubscriptions.userId, userId),
or(eq(userSubscriptions.status, "active"), eq(userSubscriptions.status, "trial")),
),
)
.limit(1);
if (!sub || !sub.endDate) return;

View File

@@ -3,6 +3,7 @@ import { VehiclesService } from "./vehicles.service";
import { CategoriesService } from "../categories/categories.service";
import { CurrentUser } from "../common/decorators/current-user.decorator";
import { VinValidationPipe } from "../common/pipes/vin-validation.pipe";
import { Public } from "../common/decorators/public.decorator";
@Controller("vehicles")
export class VehiclesController {
@@ -11,6 +12,12 @@ export class VehiclesController {
private categoriesService: CategoriesService,
) {}
@Public()
@Get("preview/:vin")
async preview(@Param("vin", VinValidationPipe) vin: string) {
return this.vehiclesService.previewVin(vin);
}
@Post("decode")
async decode(
@CurrentUser("id") userId: string,

View File

@@ -176,6 +176,59 @@ export class VehiclesService {
return savedVehicle;
}
/**
* Public VIN preview — no auth, no DB save, no brand access check.
* Uses Corgi → PL24 → EMEX decode chain, returns basic vehicle info.
*/
async previewVin(vin: string) {
if (!isValidVin(vin)) {
throw new BadRequestException("Invalid VIN");
}
// 1. Corgi decode (offline)
const corgiResult = this.corgiService.decodeVin(vin);
let brandName = corgiResult?.isKnown ? corgiResult.brandName : null;
// 2. PL24 decode
let pl24Vehicle: any = null;
if (this.pl24Service.isSupported(vin)) {
try {
pl24Vehicle = await this.pl24Service.decodeVin(vin);
if (!brandName && pl24Vehicle) {
brandName = this.pl24Service.getBrandName(vin) || null;
}
} catch (err) {
this.logger.warn(`PL24 preview failed for ${vin}: ${(err as Error).message}`);
}
}
// 3. EMEX fallback
let emexVehicle: import('../integrations/emex/emex.types').DecodedVehicle | null = null;
if (!pl24Vehicle) {
try {
const emexResult = await this.emexService.decodeVin(vin);
if (emexResult && emexResult.brand !== 'UNKNOWN') {
emexVehicle = emexResult;
if (!brandName) brandName = emexResult.brand || null;
}
} catch (err) {
this.logger.warn(`EMEX preview failed for ${vin}: ${(err as Error).message}`);
}
}
if (!pl24Vehicle && !emexVehicle && !corgiResult?.isKnown) {
throw new BadRequestException("VIN not recognized");
}
return {
brandName: brandName || corgiResult?.brandName || emexVehicle?.brand || null,
model: pl24Vehicle?.model || emexVehicle?.model || null,
year: pl24Vehicle?.year || emexVehicle?.year || corgiResult?.modelYear || null,
engine: pl24Vehicle?.engineType || pl24Vehicle?.engineCode || emexVehicle?.engineCode || emexVehicle?.engineType || null,
source: pl24Vehicle ? "pl24" : emexVehicle ? "emex" : "corgi",
};
}
async getHistory(userId: string, page = 1, limit = 20) {
const offset = (page - 1) * limit;
return this.db