feat: Update Vite configuration and add new public routes

- Refactored vite.config.ts to update entry points and improve plugin configuration.
- Added Home.vue as the landing page with a modern design and features overview.
- Created Layout.vue for consistent header and footer across public routes.
- Implemented Privacy.vue and Terms.vue for legal documentation with structured content.
- Introduced gRPC and service index files for future service implementations.
- Developed createVueApp.ts for application setup with PrimeVue and Pinia.
- Enhanced SSR rendering in ssrRender.ts for improved performance and SEO.
This commit is contained in:
2026-01-06 15:39:38 +07:00
parent 518651a93e
commit 5fc4bef5be
20 changed files with 876 additions and 654 deletions

View File

@@ -2,11 +2,9 @@
"name": "holistream", "name": "holistream",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "bunx --bun vite",
"build": "vite build && bun build dist/server/index.js --target bun --minify --outdir dist", "build": "bunx --bun vite build && bun build dist/server/index.js --target bun --minify --outdir dist",
"preview": "vite preview", "preview": "bunx --bun vite preview"
"deploy": "wrangler deploy",
"cf-typegen": "wrangler types --env-interface CloudflareBindings"
}, },
"dependencies": { "dependencies": {
"@aws-sdk/client-s3": "^3.946.0", "@aws-sdk/client-s3": "^3.946.0",

View File

@@ -1,7 +1,7 @@
import { createApp } from './main';
import 'uno.css'; import 'uno.css';
import createVueApp from './shared/createVueApp';
async function render() { async function render() {
const { app, router } = createApp(); const { app, router } = createVueApp();
router.isReady().then(() => { router.isReady().then(() => {
app.mount('body', true) app.mount('body', true)
}) })

View File

@@ -19,12 +19,11 @@ const links = [
{ href: "/upload", label: "Upload", icon: Upload, type: "a" }, { href: "/upload", label: "Upload", icon: Upload, type: "a" },
{ href: "/video", label: "Video", icon: Video, type: "a" }, { href: "/video", label: "Video", icon: Video, type: "a" },
{ href: "/plans", label: "Plans", icon: Credit, type: "a" }, { href: "/plans", label: "Plans", icon: Credit, type: "a" },
{ href: "/notification", label: "Notification", icon: Bell, type: "a" }, // { href: "/notification", label: "Notification", icon: Bell, type: "a" },
]; ];
</script> </script>
<template> <template>
<header <header class=":uno: fixed left-0 w-18 flex flex-col items-center pt-4 gap-6 z-41 max-h-screen h-screen border-r border-gray-200 bg-white">
class=":uno: fixed left-0 w-18 flex flex-col items-center pt-4 gap-6 z-41 max-h-screen h-screen border-r border-gray-200 bg-white">
<component :is="i.type === 'a' ? 'router-link' : 'div'" v-for="i in links" :key="i.label" <component :is="i.type === 'a' ? 'router-link' : 'div'" v-for="i in links" :key="i.label"
v-bind="i.type === 'a' ? { to: i.href } : {}" v-tooltip="i.label" v-bind="i.type === 'a' ? { to: i.href } : {}" v-tooltip="i.label"
:class="cn(className, $route.path === i.href && 'bg-primary/15')"> :class="cn(className, $route.path === i.href && 'bg-primary/15')">

View File

@@ -1,91 +0,0 @@
import { Hono } from 'hono'
import { createApp } from './main';
import { renderToWebStream } from 'vue/server-renderer';
import { streamText } from 'hono/streaming';
import { renderSSRHead } from '@unhead/vue/server';
import { buildBootstrapScript, getHrefFromManifest, loadCssByModules } from './lib/manifest';
import { contextStorage } from 'hono/context-storage';
import { cors } from "hono/cors";
import { jwtRpc, rpcServer } from './api/rpc';
import isMobile from 'is-mobile';
import { useAuthStore } from './stores/auth';
// import { serveStatic } from "hono/bun";
import { serveStatic } from "hono/serve-static";
import { cssContent } from './lib/primeCssContent';
import { styleTags } from './lib/primePassthrough';
// @ts-ignore
import Base from '@primevue/core/base';
const app = new Hono()
const defaultNames = ['primitive', 'semantic', 'global', 'base', 'ripple-directive']
const isDev = import.meta.env.DEV;
console.log("process.versions?.bun:", (process as any).versions?.bun);
// app.use(renderer)
app.use('*', contextStorage());
app.use(cors(), async (c, next) => {
c.set("fetch", app.request.bind(app));
const ua = c.req.header("User-Agent")
if (!ua) {
return c.json({ error: "User-Agent header is missing" }, 400);
};
c.set("isMobile", isMobile({ ua }));
await next();
}, rpcServer);
if (!isDev) {
if ((process as any).versions?.bun) {
const { serveStatic } = await import("hono/bun");
app.use(serveStatic({ root: "./dist/client" }))
}
}
app.get("/.well-known/*", (c) => {
return c.json({ ok: true });
});
app.get("*", async (c) => {
const nonce = crypto.randomUUID();
const url = new URL(c.req.url);
const { app, router, head, pinia, bodyClass } = createApp();
app.provide("honoContext", c);
const auth = useAuthStore();
auth.$reset();
auth.initialized = false;
await auth.init();
await router.push(url.pathname);
await router.isReady();
let usedStyles = new Set<String>();
Base.setLoadedStyleName = async (name: string) => usedStyles.add(name)
return streamText(c, async (stream) => {
c.header("Content-Type", "text/html; charset=utf-8");
c.header("Content-Encoding", "Identity");
const ctx: Record<string, any> = {};
const appStream = renderToWebStream(app, ctx);
// console.log("ctx: ", );
await stream.write("<!DOCTYPE html><html lang='en'><head>");
await stream.write("<base href='" + url.origin + "'/>");
await renderSSRHead(head).then((headString) => stream.write(headString.headTags.replace(/\n/g, "")));
await stream.write(`<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"rel="stylesheet"></link>`);
await stream.write('<link rel="icon" href="/favicon.ico" />');
await stream.write(buildBootstrapScript());
if (usedStyles.size > 0) {
defaultNames.forEach(name => usedStyles.add(name));
}
await Promise.all(styleTags.filter(tag => usedStyles.has(tag.name.replace(/-(variables|style)$/, ""))).map(tag => stream.write(`<style type="text/css" data-primevue-style-id="${tag.name}">${tag.value}</style>`)));
await stream.write(`</head><body class='${bodyClass}'>`);
await stream.pipe(appStream);
Object.assign(ctx, { $p: pinia.state.value });
await stream.write(`<script type="application/json" data-ssr="true" id="__APP_DATA__" nonce="${nonce}">${htmlEscape((JSON.stringify(ctx)))}</script>`);
await stream.write("</body></html>");
});
})
const ESCAPE_LOOKUP: { [match: string]: string } = {
"&": "\\u0026",
">": "\\u003e",
"<": "\\u003c",
"\u2028": "\\u2028",
"\u2029": "\\u2029",
};
const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
function htmlEscape(str: string): string {
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
}
export default app

View File

@@ -1,56 +1,33 @@
import { createHead as CSRHead } from "@unhead/vue/client"; import { Hono } from 'hono';
import { createHead as SSRHead } from "@unhead/vue/server"; import { contextStorage } from 'hono/context-storage';
import { createSSRApp } from 'vue'; import { cors } from "hono/cors";
import { RouterView } from 'vue-router'; import isMobile from 'is-mobile';
import { withErrorBoundary } from './lib/hoc/withErrorBoundary'; import { rpcServer } from './api/rpc';
import { vueSWR } from './lib/swr/use-swrv'; import { ssrRender } from './worker/ssrRender';
import createAppRouter from './routes'; // import { serveStatic } from "hono/bun";
import PrimeVue from 'primevue/config'; // @ts-ignore
import Aura from '@primeuix/themes/aura'; const app = new Hono()
import { createPinia } from "pinia"; const isDev = import.meta.env.DEV;
import { useAuthStore } from './stores/auth'; console.log("process.versions?.bun:", (process as any).versions?.bun);
import ToastService from 'primevue/toastservice'; // app.use(renderer)
import Tooltip from 'primevue/tooltip'; app.use('*', contextStorage());
const bodyClass = ":uno: font-sans text-gray-800 antialiased flex flex-col min-h-screen" app.use(cors(), async (c, next) => {
export function createApp() { c.set("fetch", app.request.bind(app));
const pinia = createPinia(); const ua = c.req.header("User-Agent")
const app = createSSRApp(withErrorBoundary(RouterView)); if (!ua) {
const head = import.meta.env.SSR ? SSRHead() : CSRHead(); return c.json({ error: "User-Agent header is missing" }, 400);
};
app.use(head); c.set("isMobile", isMobile({ ua }));
app.use(PrimeVue, { await next();
// unstyled: true, }, rpcServer);
theme: { if (!isDev) {
preset: Aura, if ((process as any).versions?.bun) {
options: { const { serveStatic } = await import("hono/bun");
darkModeSelector: '.my-app-dark', app.use(serveStatic({ root: "./dist/client" }))
cssLayer: false, }
// cssLayer: { }
// name: 'primevue', app.get("/.well-known/*", (c) => {
// order: 'theme, base, primevue' return c.json({ ok: true });
// } });
} app.get("*", ssrRender);
} export default app
});
app.use(ToastService);
app.directive('nh', {
created(el) {
el.__v_skip = true;
}
});
app.directive("tooltip", Tooltip)
if (!import.meta.env.SSR) {
Object.entries(JSON.parse(document.getElementById("__APP_DATA__")?.innerText || "{}")).forEach(([key, value]) => {
(window as any)[key] = value;
});
if ((window as any).$p ) {
pinia.state.value = (window as any).$p;
}
}
app.use(pinia);
app.use(vueSWR({revalidateOnFocus: false}));
const router = createAppRouter();
app.use(router);
return { app, router, head, pinia, bodyClass };
}

View File

@@ -1,5 +1,5 @@
<template> <template>
<div class="w-full max-w-md bg-white p-8 rounded-xl border border-primary m-auto overflow-hidden"> <div class="w-full max-w-md bg-white p-8 rounded-xl border border-gray-200 m-auto overflow-hidden">
<div class="text-center mb-8"> <div class="text-center mb-8">
<router-link to="/" class="inline-flex items-center justify-center w-12 h-12 mb-4"> <router-link to="/" class="inline-flex items-center justify-center w-12 h-12 mb-4">
<img class="w-12 h-12" src="/apple-touch-icon.png" alt="Logo" /> <img class="w-12 h-12" src="/apple-touch-icon.png" alt="Logo" />

View File

@@ -1,234 +0,0 @@
<template>
<nav class="fixed w-full z-50 glass-nav transition-all duration-300" id="navbar">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<div class="flex items-center gap-2 cursor-pointer" onclick="window.scrollTo(0,0)"><img class="h-8 w-8" src="/apple-touch-icon.png" alt="Logo" />
<span class="font-bold text-xl tracking-tight text-slate-900">EcoStream</span>
</div>
<div class="hidden md:flex items-center space-x-8">
<a href="#features" class="text-sm font-medium text-slate-600 hover:text-brand-600 transition-colors">Features</a>
<a href="#pricing" class="text-sm font-medium text-slate-600 hover:text-brand-600 transition-colors">Pricing</a>
</div>
<div class="hidden md:flex items-center gap-4">
<RouterLink to="/login" class="text-sm font-semibold text-slate-600 hover:text-slate-900 cursor-pointer">Log in</RouterLink>
<RouterLink to="/sign-up" class="bg-slate-900 hover:bg-black text-white px-5 py-2.5 rounded-lg text-sm font-semibold cursor-pointer">
Start for free
</RouterLink>
</div>
</div>
</div>
</nav>
<section class="relative pt-32 pb-20 lg:pt-48 lg:pb-32 overflow-hidden">
<div class="absolute inset-0 opacity-[0.4] -z-10"></div>
<div class="absolute top-0 right-0 -translate-y-1/2 translate-x-1/2 w-[800px] h-[800px] bg-brand-100/50 rounded-full blur-3xl -z-10 mix-blend-multiply"></div>
<div class="absolute bottom-0 left-0 translate-y-1/2 -translate-x-1/2 w-[600px] h-[600px] bg-teal-100/50 rounded-full blur-3xl -z-10 mix-blend-multiply"></div>
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 text-center">
<h1 class="text-5xl md:text-7xl font-extrabold tracking-tight text-slate-900 mb-6 leading-[1.1]">
Video infrastructure for <br>
<span class="text-gradient">modern internet.</span>
</h1>
<p class="text-xl text-slate-500 max-w-2xl mx-auto mb-10 leading-relaxed">
Seamlessly host, encode, and stream video with our developer-first API.
Optimized for speed, built for scale.
</p>
<div class="flex flex-col sm:flex-row justify-center gap-4">
<RouterLink to="/get-started" class="flex btn btn-secondary !rounded-xl !p-4 press-animated">
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="46 -286 524 580"><path d="M56 284v-560L560 4 56 284z" fill="#fff"/></svg>&nbsp;
Get Started
</RouterLink>
<RouterLink to="/docs" class="flex btn btn-outline-primary !rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="-10 -261 468 503"><path d="M256-139V72c0 18-14 32-32 32s-32-14-32-32v-211l-41 42c-13 12-33 12-46 0-12-13-12-33 0-46l96-96c13-12 33-12 46 0l96 96c12 13 12 33 0 46-13 12-33 12-46 0l-41-42zm-32 291c44 0 80-36 80-80h80c35 0 64 29 64 64v32c0 35-29 64-64 64H64c-35 0-64-29-64-64v-32c0-35 29-64 64-64h80c0 44 36 80 80 80zm144 24c13 0 24-11 24-24s-11-24-24-24-24 11-24 24 11 24 24 24z" fill="#14a74b"/></svg>&nbsp;
Upload video
</RouterLink>
</div>
</div>
</section>
<section id="features" class="py-24 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="mb-16 md:text-center max-w-3xl mx-auto">
<h2 class="text-3xl font-bold text-slate-900 mb-4">Everything you need to ship video</h2>
<p class="text-lg text-slate-500">Focus on building your product. We'll handle the complex video infrastructure.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div class="md:col-span-2 bg-slate-50 rounded-2xl p-8 border border-slate-100 hover:border-primary/60 transition-all group overflow-hidden relative">
<div class="relative z-10">
<div class="w-12 h-12 bg-white rounded-xl flex items-center justify-center mb-6 border border-slate-100">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="532" viewBox="-8 -258 529 532"><path d="M342 32c-2 69-16 129-35 172-10 23-22 40-32 49-10 10-16 11-19 11h-1c-3 0-9-1-19-11-10-9-22-26-32-49-19-43-33-103-35-172h173zm169 0c-9 103-80 188-174 219 30-51 50-129 53-219h121zm-390 0c3 89 23 167 53 218C80 219 11 134 2 32h119zm53-266c-30 51-50 129-53 218H2c9-102 78-186 172-218zm82-14c3 0 9 1 19 11 10 9 22 26 32 50 19 42 33 102 35 171H169c3-69 16-129 35-171 10-24 22-41 32-50s16-11 19-11h1zm81 13c94 31 165 116 174 219H390c-3-90-23-168-53-219z" fill="#059669"/></svg>
</div>
<h3 class="text-xl font-bold text-slate-900 mb-2">Global Edge Network</h3>
<p class="text-slate-500 max-w-md">Content delivered from 200+ PoPs worldwide. Automatic region selection ensures the lowest latency for every viewer.</p>
</div>
<div class="absolute right-0 bottom-0 opacity-10 translate-x-1/4 translate-y-1/4">
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="-10 -258 532 532"><path d="M464 8c0-19-3-38-8-56l-27-5c-8-2-15 2-19 9-6 11-19 17-31 13l-14-5c-8-2-17 0-22 5-4 4-4 10 0 14l33 33c5 5 8 12 8 19 0 12-8 23-20 26l-6 1c-3 1-6 5-6 9v12c0 13-4 27-13 38l-25 34c-6 8-16 13-26 13-18 0-32-14-32-32V88c0-9-7-16-16-16h-32c-26 0-48-22-48-48V-4c0-13 6-24 16-32l39-30c6-4 13-6 20-6 3 0 7 1 10 2l32 10c7 3 15 3 22 1l36-9c10-2 17-11 17-22 0-8-5-16-13-20l-29-15c-3-2-8-1-11 2l-4 4c-4 4-11 7-17 7-4 0-8-1-11-3l-15-7c-7-4-15-2-20 4l-13 17c-6 7-16 8-22 1-3-2-5-6-5-10v-41c0-6-1-11-4-16l-10-18C102-154 48-79 48 8c0 115 93 208 208 208S464 123 464 8zM0 8c0-141 115-256 256-256S512-133 512 8 397 264 256 264 0 149 0 8z" fill="#1e3050"/></svg>
</div>
</div>
<div class="md:row-span-2 bg-slate-900 rounded-2xl p-8 text-white relative overflow-hidden group">
<div class="absolute inset-0 bg-gradient-to-b from-slate-800/50 to-transparent"></div>
<div class="relative z-10">
<div class="w-12 h-12 bg-white/10 rounded-xl flex items-center justify-center mb-6 backdrop-blur-sm border border-white/10">
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="-10 -146 468 384"><path d="M392-136c-31 0-56 25-56 56v280c0 16 13 28 28 28h28c31 0 56-25 56-56V-80c0-31-25-56-56-56zM168 4c0-31 25-56 56-56h28c16 0 28 13 28 28v224c0 16-12 28-28 28h-56c-15 0-28-12-28-28V4zM0 88c0-31 25-56 56-56h28c16 0 28 13 28 28v140c0 16-12 28-28 28H56c-31 0-56-25-56-56V88z" fill="#fff"/></svg>
</div>
<h3 class="text-xl font-bold mb-2">Live Streaming API</h3>
<p class="text-slate-400 text-sm leading-relaxed mb-8">Scale to millions of concurrent viewers with ultra-low latency. RTMP ingest and HLS playback supported natively.</p>
<!-- Visual -->
<div class="bg-slate-800/50 rounded-lg p-4 border border-white/5 font-mono text-xs text-brand-300">
<div class="flex justify-between items-center mb-3 border-b border-white/5 pb-2">
<span class="text-slate-500">Live Status</span>
<span class="flex items-center gap-1.5 text-red-500 text-[10px] uppercase font-bold tracking-wider animate-pulse"><span class="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse"></span> On Air</span>
</div>
<div class="space-y-1">
<div class="flex justify-between"><span class="text-slate-400">Bitrate:</span> <span class="text-white">6000 kbps</span></div>
<div class="flex justify-between"><span class="text-slate-400">FPS:</span> <span class="text-white">60</span></div>
<div class="flex justify-between"><span class="text-slate-400">Latency:</span> <span class="text-brand-400">~2s</span></div>
</div>
</div>
</div>
</div>
<!-- Standard Feature -->
<div class="bg-slate-50 rounded-2xl p-8 border border-slate-100 hover:border-brand-200 transition-all group hover:shadow-lg hover:shadow-brand-500/5">
<div class="w-12 h-12 bg-white rounded-xl shadow-sm flex items-center justify-center mb-6 text-purple-600 border border-slate-100">
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="0 0 570 570"><path d="M50 428c-5 5-5 14 0 19s14 5 19 0l237-237c5-5 5-14 0-19s-14-5-19 0L50 428zm16-224c-5 5-5 13 0 19 5 5 14 5 19 0l12-12c5-5 5-14 0-19-6-5-14-5-20 0l-11 12zM174 60c-5 5-5 13 0 19 5 5 14 5 19 0l12-12c5-5 5-14 0-19-6-5-14-5-20 0l-11 12zm215 29c-5 5-5 14 0 19s14 5 19 0l39-39c5-5 5-14 0-19s-14-5-19 0l-39 39zm21 357c-5 5-5 14 0 19s14 5 19 0l18-18c5-5 5-14 0-19s-14-5-19 0l-18 18z" fill="#a6acb9"/><path d="M170 26c14-15 36-15 50 0l18 18c15 14 15 36 0 50l-18 18c-14 15-36 15-50 0l-18-18c-15-14-15-36 0-50l18-18zm35 41c5-5 5-14 0-19-6-5-14-5-20 0l-11 12c-5 5-5 13 0 19 5 5 14 5 19 0l12-12zm204 342c21-21 55-21 76 0l18 18c21 21 21 55 0 76l-18 18c-21 21-55 21-76 0l-18-18c-21-21-21-55 0-76l18-18zm38 38c5-5 5-14 0-19s-14-5-19 0l-18 18c-5 5-5 14 0 19s14 5 19 0l18-18zM113 170c-15-15-37-15-51 0l-18 18c-14 14-14 36 0 50l18 18c14 15 37 15 51 0l18-18c14-14 14-36 0-50l-18-18zm-16 41-12 12c-5 5-14 5-19 0-5-6-5-14 0-20l11-11c6-5 14-5 20 0 5 5 5 14 0 19zM485 31c-21-21-55-21-76 0l-39 39c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l39-39c21-21 21-55 0-76l-54-54zm-38 38-39 39c-5 5-14 5-19 0s-5-14 0-19l39-39c5-5 14-5 19 0s5 14 0 19zm-49 233c21-21 21-55 0-76l-54-54c-21-21-55-21-76 0L31 409c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l237-237zm-92-92L69 447c-5 5-14 5-19 0s-5-14 0-19l237-237c5-5 14-5 19 0s5 14 0 19z" fill="#1e3050"/></svg>
</div>
<h3 class="text-xl font-bold text-slate-900 mb-2">Instant Encoding</h3>
<p class="text-slate-500 text-sm">Upload raw files and get optimized HLS/DASH streams in seconds.</p>
</div>
<!-- Standard Feature -->
<div class="bg-slate-50 rounded-2xl p-8 border border-slate-100 hover:border-brand-200 transition-all group hover:shadow-lg hover:shadow-brand-500/5">
<div class="w-12 h-12 bg-white rounded-xl shadow-sm flex items-center justify-center mb-6 text-orange-600 border border-slate-100">
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="-10 -226 532 468"><path d="M32-216c18 0 32 14 32 32v336c0 9 7 16 16 16h400c18 0 32 14 32 32s-14 32-32 32H80c-44 0-80-36-80-80v-336c0-18 14-32 32-32zM144-24c18 0 32 14 32 32v64c0 18-14 32-32 32s-32-14-32-32V8c0-18 14-32 32-32zm144-64V72c0 18-14 32-32 32s-32-14-32-32V-88c0-18 14-32 32-32s32 14 32 32zm80 32c18 0 32 14 32 32v96c0 18-14 32-32 32s-32-14-32-32v-96c0-18 14-32 32-32zm144-96V72c0 18-14 32-32 32s-32-14-32-32v-224c0-18 14-32 32-32s32 14 32 32z" fill="#1e3050"/></svg>
</div>
<h3 class="text-xl font-bold text-slate-900 mb-2">Deep Analytics</h3>
<p class="text-slate-500 text-sm">Session-level insights, quality of experience (QoE) metrics, and more.</p>
</div>
</div>
</div>
</section>
<!-- Pricing -->
<section id="pricing" class="py-24 border-t border-slate-100 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<h2 class="text-3xl font-bold text-slate-900 mb-4">{{ pricing.title }}</h2>
<p class="text-slate-500">{{ pricing.subtitle }}</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 w-full">
<div v-for="pack in pricing.packs" :key="pack.name" :class="cn(':uno: p-8 rounded-2xl relative overflow-hidden hover:border-primary transition-colors flex flex-col justify-between', pack.tag == 'POPULAR' ? 'border-primary/80 border-2' : 'border-slate-200 border')" :style="{background: pack.bg}">
<div v-if="pack.tag" class="absolute top-0 right-0 bg-primary/80 text-white text-xs font-bold px-3 py-1 rounded-bl-lg uppercase">{{ pack.tag }}</div>
<div>
<h3 class="font-semibold text-slate-900 text-xl mb-2">{{ pack.name }}</h3>
<div class="flex items-baseline gap-1 mb-6">
<span class="text-4xl font-bold text-slate-900">{{ pack.price }}</span>
<span class="text-slate-500">/mo</span>
</div>
</div>
<ul class="space-y-3 mb-8 text-sm text-slate-600">
<li v-for="value in pack.features" :key="value" class="flex items-center gap-3"><Check-Icon class="fas fa-check text-brand-500"/> {{ value }}</li>
</ul>
<router-link to="/sign-up" :class="cn('btn flex justify-center w-full !py-2.5', pack.tag == 'POPULAR' ? 'btn-primary' : 'btn-outline-primary')">{{ pack.buttonText }}</router-link>
</div>
</div>
</div>
</section>
<!-- Footer -->
<footer class="bg-white border-t border-slate-100 pt-16 pb-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-2 md:grid-cols-5 gap-8 mb-12">
<div class="col-span-2">
<div class="flex items-center gap-2 mb-4">
<div class="w-6 h-6 bg-brand-600 rounded flex items-center justify-center text-white">
<img class="h-6 w-6" src="/apple-touch-icon.png" alt="Logo" />
</div>
<span class="font-bold text-lg text-slate-900">EcoStream</span>
</div>
<p class="text-slate-500 text-sm max-w-xs">Building the video layer of the internet. Designed for developers.</p>
</div>
<div>
<h4 class="font-semibold text-slate-900 mb-4 text-sm">Product</h4>
<ul class="space-y-2 text-sm text-slate-500">
<li><a href="#" class="hover:text-brand-600">Features</a></li>
<li><a href="#" class="hover:text-brand-600">Pricing</a></li>
<li><a href="#" class="hover:text-brand-600">Showcase</a></li>
</ul>
</div>
<div>
<h4 class="font-semibold text-slate-900 mb-4 text-sm">Company</h4>
<ul class="space-y-2 text-sm text-slate-500">
<li><a href="#" class="hover:text-brand-600">About</a></li>
<li><a href="#" class="hover:text-brand-600">Blog</a></li>
<li><a href="#" class="hover:text-brand-600">Careers</a></li>
</ul>
</div>
<div>
<h4 class="font-semibold text-slate-900 mb-4 text-sm">Legal</h4>
<ul class="space-y-2 text-sm text-slate-500">
<li><a href="#" class="hover:text-brand-600">Privacy</a></li>
<li><a href="#" class="hover:text-brand-600">Terms</a></li>
</ul>
</div>
</div>
<div class="pt-8 border-t border-slate-100 text-center text-sm text-slate-400">
&copy; 2026 EcoStream Inc. All rights reserved.
</div>
</div>
</footer>
<Head>
<title>EcoStream - Video infrastructure for modern internet</title>
<meta name="description" content="Seamlessly host, encode, and stream video with our developer-first API. Optimized for speed, built for scale." />
</Head>
</template>
<script lang="ts" setup>
import { Head } from '@unhead/vue/components'
import { cn } from '@/lib/utils';
const pricing = {
title: "Simple, transparent pricing",
subtitle: "Choose the plan that fits your needs. No hidden fees.",
packs: [
{
name: "Hobby",
price: "$0",
features: [
"Unlimited upload",
"1 Hour of Storage",
"Standard Support",
],
buttonText: "Start Free",
tag: "",
bg: "#f9fafb",
},
{
name: "Pro",
price: "$29",
features: [
"Ads free player",
"Support M3U8",
"Unlimited upload",
"Custom ads"
],
buttonText: "Get Started",
tag: "POPULAR",
bg: "#eff6ff",
},
{
name: "Scale",
price: "$99",
features: [
"5 TB Bandwidth",
"500 Hours Storage",
"Priority Support"
],
buttonText: "Contact Sales",
tag: "Best Value",
bg: "#eef4f7",
}
]
}
</script>

View File

@@ -1,157 +1,179 @@
import { type ReactiveHead, type ResolvableValue } from "@unhead/vue"; import { type ReactiveHead, type ResolvableValue } from "@unhead/vue";
import { headSymbol } from '@unhead/vue' import { headSymbol } from '@unhead/vue'
import { import {
createMemoryHistory, createMemoryHistory,
createRouter, createRouter,
createWebHistory, createWebHistory,
type RouteRecordRaw, type RouteRecordRaw,
} from "vue-router"; } from "vue-router";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
type RouteData = RouteRecordRaw & { type RouteData = RouteRecordRaw & {
meta?: ResolvableValue<ReactiveHead> & { requiresAuth?: boolean }; meta?: ResolvableValue<ReactiveHead> & { requiresAuth?: boolean };
children?: RouteData[]; children?: RouteData[];
}; };
const routes: RouteData[] = [ const routes: RouteData[] = [
{ {
path: "/", path: "/",
component: () => import("@/components/RootLayout.vue"), component: () => import("@/components/RootLayout.vue"),
children: [
{
path: "",
component: () => import("./home/Home.vue"),
beforeEnter: (to, from, next) => {
const auth = useAuthStore();
if (auth.user) {
next({ name: "overview" });
} else {
next();
}
},
},
{
path: "",
component: () => import("./auth/layout.vue"),
beforeEnter: (to, from, next) => {
const auth = useAuthStore();
if (auth.user) {
next({ name: "overview" });
} else {
next();
}
},
children: [ children: [
{ {
path: "login", path: "",
name: "login", component: () => import("./public-routes/Layout.vue"),
component: () => import("./auth/login.vue"), children: [
}, {
{ path: "",
path: "sign-up", component: () => import("./public-routes/Home.vue"),
name: "signup", beforeEnter: (to, from, next) => {
component: () => import("./auth/signup.vue"), const auth = useAuthStore();
}, if (auth.user) {
{ next({ name: "overview" });
path: "forgot", } else {
name: "forgot", next();
component: () => import("./auth/forgot.vue"), }
}, },
], },
}, {
{ path: "/terms",
path: "", name: "terms",
component: () => import("@/components/DashboardLayout.vue"), component: () => import("./public-routes/Terms.vue"),
meta: { requiresAuth: true }, },
children: [ {
{ path: "/privacy",
path: "", name: "privacy",
name: "overview", component: () => import("./public-routes/Privacy.vue"),
component: () => import("./add/Add.vue"), },
meta: { ]
head: { },
title: 'Overview - Holistream', {
}, path: "",
} component: () => import("./auth/layout.vue"),
}, beforeEnter: (to, from, next) => {
{ const auth = useAuthStore();
path: "upload", if (auth.user) {
name: "upload", next({ name: "overview" });
component: () => import("./add/Add.vue"), } else {
meta: { next();
head: { }
title: 'Upload - Holistream', },
}, children: [
} {
}, path: "login",
{ name: "login",
path: "video", component: () => import("./auth/login.vue"),
name: "video", },
component: () => import("./add/Add.vue"), {
meta: { path: "sign-up",
head: { name: "signup",
title: 'Videos - Holistream', component: () => import("./auth/signup.vue"),
meta: [ },
{ name: 'description', content: 'Manage your video content.' }, {
path: "forgot",
name: "forgot",
component: () => import("./auth/forgot.vue"),
},
], ],
}, },
} {
}, path: "",
{ component: () => import("@/components/DashboardLayout.vue"),
path: "plans", meta: { requiresAuth: true },
name: "plans", children: [
component: () => import("./add/Add.vue"), {
meta: { path: "",
head: { name: "overview",
title: 'Plans & Billing', component: () => import("./add/Add.vue"),
meta: [ meta: {
{ name: 'description', content: 'Manage your plans and billing information.' }, head: {
title: 'Overview - Holistream',
},
}
},
{
path: "upload",
name: "upload",
component: () => import("./add/Add.vue"),
meta: {
head: {
title: 'Upload - Holistream',
},
}
},
{
path: "video",
name: "video",
component: () => import("./add/Add.vue"),
meta: {
head: {
title: 'Videos - Holistream',
meta: [
{ name: 'description', content: 'Manage your video content.' },
],
},
}
},
{
path: "plans",
name: "plans",
component: () => import("./add/Add.vue"),
meta: {
head: {
title: 'Plans & Billing',
meta: [
{ name: 'description', content: 'Manage your plans and billing information.' },
],
},
}
},
{
path: "notification",
name: "notification",
component: () => import("./add/Add.vue"),
meta: {
head: {
title: 'Notification - Holistream',
},
}
},
], ],
}, },
{
path: "/:pathMatch(.*)*",
name: "not-found",
component: () => import("./NotFound.vue"),
} }
},
{
path: "notification",
name: "notification",
component: () => import("./add/Add.vue"),
meta: {
head: {
title: 'Notification - Holistream',
},
}
},
], ],
}, },
{
path: "/:pathMatch(.*)*",
name: "not-found",
component: () => import("./NotFound.vue"),
}
],
},
]; ];
const createAppRouter = () => { const createAppRouter = () => {
const router = createRouter({ const router = createRouter({
history: import.meta.env.SSR history: import.meta.env.SSR
? createMemoryHistory() // server ? createMemoryHistory() // server
: createWebHistory(), // client : createWebHistory(), // client
routes, routes,
}); scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition
}
return { top: 0 }
}
});
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const auth = useAuthStore(); const auth = useAuthStore();
const head = inject(headSymbol); const head = inject(headSymbol);
(head as any).push(to.meta.head || {}); (head as any).push(to.meta.head || {});
if (to.matched.some((record) => record.meta.requiresAuth)) { if (to.matched.some((record) => record.meta.requiresAuth)) {
if (!auth.user) { if (!auth.user) {
next({ name: "login" }); next({ name: "login" });
} else { } else {
next(); next();
} }
} else { } else {
next(); next();
} }
}); });
return router; return router;
} }
export default createAppRouter; export default createAppRouter;

View File

@@ -0,0 +1,231 @@
<template>
<section class="relative pt-32 pb-20 lg:pt-48 lg:pb-32 overflow-hidden min-h-svh flex">
<!-- <div class="absolute inset-0 bg-grid-pattern opacity-[0.4] -z-10"></div> -->
<div
class="absolute top-0 right-0 -translate-y-1/2 translate-x-1/2 w-[800px] h-[800px] bg-primary-light/40 rounded-full blur-3xl -z-10 mix-blend-multiply animate-pulse duration-1000">
</div>
<div
class="absolute bottom-0 left-0 translate-y-1/2 -translate-x-1/2 w-[600px] h-[600px] bg-teal-100/50 rounded-full blur-3xl -z-10 mix-blend-multiply">
</div>
<div class="max-w-7xl m-auto px-4 sm:px-6 lg:px-8 text-center">
<h1
class="text-5xl md:text-7xl font-extrabold tracking-tight text-slate-900 mb-6 leading-[1.1] animate-backwards">
Video infrastructure for <br>
<span class="text-gradient">modern internet.</span>
</h1>
<p class="text-xl text-slate-500 max-w-2xl mx-auto mb-10 leading-relaxed animate-backwards delay-50">
Seamlessly host, encode, and stream video with our developer-first API.
Optimized for speed, built for scale.
</p>
<div class="flex flex-col sm:flex-row justify-center gap-4">
<RouterLink to="/get-started" class="flex btn btn-success !rounded-xl !p-4 press-animated">
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="46 -286 524 580">
<path d="M56 284v-560L560 4 56 284z" fill="#fff" />
</svg>&nbsp;
Get Started
</RouterLink>
<RouterLink to="/docs" class="flex btn btn-outline-primary !rounded-xl">
<svg xmlns="http://www.w3.org/2000/svg" width="28" viewBox="0 0 596 468">
<path
d="M10 314c0-63 41-117 98-136-1-8-2-16-2-24 0-79 65-144 144-144 55 0 104 31 128 77 14-8 30-13 48-13 53 0 96 43 96 96 0 16-4 31-10 44 44 20 74 64 74 116 0 71-57 128-128 128H154c-79 0-144-64-144-144zm199-73c-9 9-9 25 0 34s25 9 34 0l31-31v102c0 13 11 24 24 24s24-11 24-24V244l31 31c9 9 25 9 34 0s9-25 0-34l-72-72c-10-9-25-9-34 0l-72 72z"
fill="#14a74b" />
<path
d="M281 169c9-9 25-9 34 0l72 72c9 9 9 25 0 34s-25 9-34 0l-31-31v102c0 13-11 24-24 24s-24-11-24-24V244l-31 31c-9 9-25 9-34 0s-9-25 0-34l72-72z"
fill="#fff" />
</svg>&nbsp;
Upload video
</RouterLink>
</div>
</div>
</section>
<section id="features" class="py-24 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="mb-16 md:text-center max-w-3xl mx-auto">
<h2 class="text-3xl font-bold text-slate-900 mb-4">Everything you need to ship video</h2>
<p class="text-lg text-slate-500">Focus on building your product. We'll handle the complex video
infrastructure.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<div
class="md:col-span-2 bg-slate-50 rounded-2xl p-8 border border-slate-100 hover:border-primary/60 transition-all group overflow-hidden relative">
<div class="relative z-10">
<div
class="w-12 h-12 bg-white rounded-xl flex items-center justify-center mb-6 border border-slate-100">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="532" viewBox="-8 -258 529 532">
<path
d="M342 32c-2 69-16 129-35 172-10 23-22 40-32 49-10 10-16 11-19 11h-1c-3 0-9-1-19-11-10-9-22-26-32-49-19-43-33-103-35-172h173zm169 0c-9 103-80 188-174 219 30-51 50-129 53-219h121zm-390 0c3 89 23 167 53 218C80 219 11 134 2 32h119zm53-266c-30 51-50 129-53 218H2c9-102 78-186 172-218zm82-14c3 0 9 1 19 11 10 9 22 26 32 50 19 42 33 102 35 171H169c3-69 16-129 35-171 10-24 22-41 32-50s16-11 19-11h1zm81 13c94 31 165 116 174 219H390c-3-90-23-168-53-219z"
fill="#059669" />
</svg>
</div>
<h3 class="text-xl font-bold text-slate-900 mb-2">Global Edge Network</h3>
<p class="text-slate-500 max-w-md">Content delivered from 200+ PoPs worldwide. Automatic region
selection ensures the lowest latency for every viewer.</p>
</div>
<div class="absolute right-0 bottom-0 opacity-10 translate-x-1/4 translate-y-1/4">
<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200" viewBox="-10 -258 532 532">
<path
d="M464 8c0-19-3-38-8-56l-27-5c-8-2-15 2-19 9-6 11-19 17-31 13l-14-5c-8-2-17 0-22 5-4 4-4 10 0 14l33 33c5 5 8 12 8 19 0 12-8 23-20 26l-6 1c-3 1-6 5-6 9v12c0 13-4 27-13 38l-25 34c-6 8-16 13-26 13-18 0-32-14-32-32V88c0-9-7-16-16-16h-32c-26 0-48-22-48-48V-4c0-13 6-24 16-32l39-30c6-4 13-6 20-6 3 0 7 1 10 2l32 10c7 3 15 3 22 1l36-9c10-2 17-11 17-22 0-8-5-16-13-20l-29-15c-3-2-8-1-11 2l-4 4c-4 4-11 7-17 7-4 0-8-1-11-3l-15-7c-7-4-15-2-20 4l-13 17c-6 7-16 8-22 1-3-2-5-6-5-10v-41c0-6-1-11-4-16l-10-18C102-154 48-79 48 8c0 115 93 208 208 208S464 123 464 8zM0 8c0-141 115-256 256-256S512-133 512 8 397 264 256 264 0 149 0 8z"
fill="#1e3050" />
</svg>
</div>
</div>
<div class="md:row-span-2 bg-slate-900 rounded-2xl p-8 text-white relative overflow-hidden group">
<div class="absolute inset-0 bg-gradient-to-b from-slate-800/50 to-transparent"></div>
<div class="relative z-10">
<div
class="w-12 h-12 bg-white/10 rounded-xl flex items-center justify-center mb-6 backdrop-blur-sm border border-white/10">
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="-10 -146 468 384">
<path
d="M392-136c-31 0-56 25-56 56v280c0 16 13 28 28 28h28c31 0 56-25 56-56V-80c0-31-25-56-56-56zM168 4c0-31 25-56 56-56h28c16 0 28 13 28 28v224c0 16-12 28-28 28h-56c-15 0-28-12-28-28V4zM0 88c0-31 25-56 56-56h28c16 0 28 13 28 28v140c0 16-12 28-28 28H56c-31 0-56-25-56-56V88z"
fill="#fff" />
</svg>
</div>
<h3 class="text-xl font-bold mb-2">Live Streaming API</h3>
<p class="text-slate-400 text-sm leading-relaxed mb-8">Scale to millions of concurrent viewers
with ultra-low latency. RTMP ingest and HLS playback supported natively.</p>
<!-- Visual -->
<div
class="bg-slate-800/50 rounded-lg p-4 border border-white/5 font-mono text-xs text-brand-300">
<div class="flex justify-between items-center mb-3 border-b border-white/5 pb-2">
<span class="text-slate-500">Live Status</span>
<span
class="flex items-center gap-1.5 text-red-500 text-[10px] uppercase font-bold tracking-wider animate-pulse"><span
class="w-1.5 h-1.5 rounded-full bg-red-500 animate-pulse"></span> On Air</span>
</div>
<div class="space-y-1">
<div class="flex justify-between"><span class="text-slate-400">Bitrate:</span> <span
class="text-white">6000 kbps</span></div>
<div class="flex justify-between"><span class="text-slate-400">FPS:</span> <span
class="text-white">60</span></div>
<div class="flex justify-between"><span class="text-slate-400">Latency:</span> <span
class="text-brand-400">~2s</span></div>
</div>
</div>
</div>
</div>
<!-- Standard Feature -->
<div
class="bg-slate-50 rounded-2xl p-8 border border-slate-100 hover:border-brand-200 transition-all group hover:shadow-lg hover:shadow-brand-500/5">
<div
class="w-12 h-12 bg-white rounded-xl shadow-sm flex items-center justify-center mb-6 text-purple-600 border border-slate-100">
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="0 0 570 570">
<path
d="M50 428c-5 5-5 14 0 19s14 5 19 0l237-237c5-5 5-14 0-19s-14-5-19 0L50 428zm16-224c-5 5-5 13 0 19 5 5 14 5 19 0l12-12c5-5 5-14 0-19-6-5-14-5-20 0l-11 12zM174 60c-5 5-5 13 0 19 5 5 14 5 19 0l12-12c5-5 5-14 0-19-6-5-14-5-20 0l-11 12zm215 29c-5 5-5 14 0 19s14 5 19 0l39-39c5-5 5-14 0-19s-14-5-19 0l-39 39zm21 357c-5 5-5 14 0 19s14 5 19 0l18-18c5-5 5-14 0-19s-14-5-19 0l-18 18z"
fill="#a6acb9" />
<path
d="M170 26c14-15 36-15 50 0l18 18c15 14 15 36 0 50l-18 18c-14 15-36 15-50 0l-18-18c-15-14-15-36 0-50l18-18zm35 41c5-5 5-14 0-19-6-5-14-5-20 0l-11 12c-5 5-5 13 0 19 5 5 14 5 19 0l12-12zm204 342c21-21 55-21 76 0l18 18c21 21 21 55 0 76l-18 18c-21 21-55 21-76 0l-18-18c-21-21-21-55 0-76l18-18zm38 38c5-5 5-14 0-19s-14-5-19 0l-18 18c-5 5-5 14 0 19s14 5 19 0l18-18zM113 170c-15-15-37-15-51 0l-18 18c-14 14-14 36 0 50l18 18c14 15 37 15 51 0l18-18c14-14 14-36 0-50l-18-18zm-16 41-12 12c-5 5-14 5-19 0-5-6-5-14 0-20l11-11c6-5 14-5 20 0 5 5 5 14 0 19zM485 31c-21-21-55-21-76 0l-39 39c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l39-39c21-21 21-55 0-76l-54-54zm-38 38-39 39c-5 5-14 5-19 0s-5-14 0-19l39-39c5-5 14-5 19 0s5 14 0 19zm-49 233c21-21 21-55 0-76l-54-54c-21-21-55-21-76 0L31 409c-21 21-21 55 0 76l54 54c21 21 55 21 76 0l237-237zm-92-92L69 447c-5 5-14 5-19 0s-5-14 0-19l237-237c5-5 14-5 19 0s5 14 0 19z"
fill="#1e3050" />
</svg>
</div>
<h3 class="text-xl font-bold text-slate-900 mb-2">Instant Encoding</h3>
<p class="text-slate-500 text-sm">Upload raw files and get optimized HLS/DASH streams in seconds.
</p>
</div>
<!-- Standard Feature -->
<div
class="bg-slate-50 rounded-2xl p-8 border border-slate-100 hover:border-brand-200 transition-all group hover:shadow-lg hover:shadow-brand-500/5">
<div
class="w-12 h-12 bg-white rounded-xl shadow-sm flex items-center justify-center mb-6 text-orange-600 border border-slate-100">
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="-10 -226 532 468">
<path
d="M32-216c18 0 32 14 32 32v336c0 9 7 16 16 16h400c18 0 32 14 32 32s-14 32-32 32H80c-44 0-80-36-80-80v-336c0-18 14-32 32-32zM144-24c18 0 32 14 32 32v64c0 18-14 32-32 32s-32-14-32-32V8c0-18 14-32 32-32zm144-64V72c0 18-14 32-32 32s-32-14-32-32V-88c0-18 14-32 32-32s32 14 32 32zm80 32c18 0 32 14 32 32v96c0 18-14 32-32 32s-32-14-32-32v-96c0-18 14-32 32-32zm144-96V72c0 18-14 32-32 32s-32-14-32-32v-224c0-18 14-32 32-32s32 14 32 32z"
fill="#1e3050" />
</svg>
</div>
<h3 class="text-xl font-bold text-slate-900 mb-2">Deep Analytics</h3>
<p class="text-slate-500 text-sm">Session-level insights, quality of experience (QoE) metrics, and
more.</p>
</div>
</div>
</div>
</section>
<!-- Pricing -->
<section id="pricing" class="py-24 border-t border-slate-100 bg-white">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16">
<h2 class="text-3xl font-bold text-slate-900 mb-4">{{ pricing.title }}</h2>
<p class="text-slate-500">{{ pricing.subtitle }}</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 w-full">
<div v-for="pack in pricing.packs" :key="pack.name"
:class="cn(':uno: p-8 rounded-2xl relative overflow-hidden hover:border-primary transition-colors flex flex-col justify-between', pack.tag == 'POPULAR' ? 'border-primary/80 border-2' : 'border-slate-200 border')"
:style="{ background: pack.bg }">
<div v-if="pack.tag"
class="absolute top-0 right-0 bg-primary/80 text-white text-xs font-bold px-3 py-1 rounded-bl-lg uppercase">
{{ pack.tag }}</div>
<div>
<h3 class="font-semibold text-slate-900 text-xl mb-2">{{ pack.name }}</h3>
<div class="flex items-baseline gap-1 mb-6">
<span class="text-4xl font-bold text-slate-900">{{ pack.price }}</span>
<span class="text-slate-500">/mo</span>
</div>
</div>
<ul class="space-y-3 mb-8 text-sm text-slate-600">
<li v-for="value in pack.features" :key="value" class="flex items-center gap-3"><Check-Icon
class="fas fa-check text-brand-500" /> {{ value }}</li>
</ul>
<router-link to="/sign-up"
:class="cn('btn flex justify-center w-full !py-2.5', pack.tag == 'POPULAR' ? 'btn-primary' : 'btn-outline-primary')">{{
pack.buttonText }}</router-link>
</div>
</div>
</div>
</section>
</template>
<script lang="ts" setup>
import { Head } from '@unhead/vue/components'
import { cn } from '@/lib/utils';
const pricing = {
title: "Simple, transparent pricing",
subtitle: "Choose the plan that fits your needs. No hidden fees.",
packs: [
{
name: "Hobby",
price: "$0",
features: [
"Unlimited upload",
"1 Hour of Storage",
"Standard Support",
],
buttonText: "Start Free",
tag: "",
bg: "#f9fafb",
},
{
name: "Pro",
price: "$29",
features: [
"Ads free player",
"Support M3U8",
"Unlimited upload",
"Custom ads"
],
buttonText: "Get Started",
tag: "POPULAR",
bg: "#eff6ff",
},
{
name: "Scale",
price: "$99",
features: [
"5 TB Bandwidth",
"500 Hours Storage",
"Priority Support"
],
buttonText: "Contact Sales",
tag: "Best Value",
bg: "#eef4f7",
}
]
}
</script>

View File

@@ -0,0 +1,84 @@
<template>
<header>
<nav class="fixed w-full z-50 glass-nav transition-all duration-300" id="navbar">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-16">
<router-link to="/" class="flex items-center gap-2 cursor-pointer">
<img class="h-8 w-8" src="/apple-touch-icon.png" alt="Logo" />
<span class="font-bold text-xl tracking-tight text-slate-900">EcoStream</span>
</router-link>
<div class="hidden md:flex items-center space-x-8">
<a href="#features"
class="text-sm font-medium text-slate-600 hover:text-brand-600 transition-colors">Features</a>
<a href="#pricing"
class="text-sm font-medium text-slate-600 hover:text-brand-600 transition-colors">Pricing</a>
</div>
<div class="hidden md:flex items-center gap-4">
<RouterLink to="/login"
class="text-sm font-semibold text-slate-600 hover:text-slate-900 cursor-pointer">Log in
</RouterLink>
<RouterLink to="/sign-up"
class="bg-slate-900 hover:bg-black text-white px-5 py-2.5 rounded-lg text-sm font-semibold cursor-pointer">
Start for free
</RouterLink>
</div>
</div>
</div>
</nav>
</header>
<main class="animate-fade-in delay-50 grow">
<router-view />
</main>
<!-- Footer -->
<footer class="bg-white border-t border-slate-100 pt-16 pb-8">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="grid grid-cols-2 md:grid-cols-5 gap-8 mb-12">
<div class="col-span-2">
<div class="flex items-center gap-2 mb-4">
<div class="w-6 h-6 bg-brand-600 rounded flex items-center justify-center text-white">
<img class="h-6 w-6" src="/apple-touch-icon.png" alt="Logo" />
</div>
<span class="font-bold text-lg text-slate-900">EcoStream</span>
</div>
<p class="text-slate-500 text-sm max-w-xs">Building the video layer of the internet. Designed for
developers.</p>
</div>
<div>
<h4 class="font-semibold text-slate-900 mb-4 text-sm">Product</h4>
<ul class="space-y-2 text-sm text-slate-500">
<li><a href="#" class="hover:text-brand-600">Features</a></li>
<li><a href="#" class="hover:text-brand-600">Pricing</a></li>
<li><a href="#" class="hover:text-brand-600">Showcase</a></li>
</ul>
</div>
<div>
<h4 class="font-semibold text-slate-900 mb-4 text-sm">Company</h4>
<ul class="space-y-2 text-sm text-slate-500">
<li><a href="#" class="hover:text-brand-600">About</a></li>
<li><a href="#" class="hover:text-brand-600">Blog</a></li>
<li><a href="#" class="hover:text-brand-600">Careers</a></li>
</ul>
</div>
<div>
<h4 class="font-semibold text-slate-900 mb-4 text-sm">Legal</h4>
<ul class="space-y-2 text-sm text-slate-500">
<li><router-link to="/privacy" class="hover:text-brand-600">Privacy</router-link></li>
<li><router-link to="/terms" class="hover:text-brand-600">Terms</router-link></li>
</ul>
</div>
</div>
<div class="pt-8 border-t border-slate-100 text-center text-sm text-slate-400">
&copy; 2026 EcoStream Inc. All rights reserved.
</div>
</div>
</footer>
<Head>
<title>EcoStream - Video infrastructure for modern internet</title>
<meta name="description"
content="Seamlessly host, encode, and stream video with our developer-first API. Optimized for speed, built for scale." />
</Head>
</template>
<script lang="ts" setup>
import { Head } from '@unhead/vue/components'
</script>

View File

@@ -0,0 +1,61 @@
<template>
<div class="max-w-4xl mx-auto space-y-10" style="opacity: 1; transform: none;">
<div class="grow pt-32 pb-12 px-4">
<div class="max-w-4xl mx-auto space-y-10">
<div class="space-y-3">
<p
class="inline-block px-4 py-1.5 rounded-full bg-info/20 font-bold text-sm uppercase">
{{ pageContent.data.pageSubheading }}</p>
<h1 class="text-4xl md:text-5xl font-heading font-extrabold">{{ pageContent.data.pageHeading }}</h1>
<p class="text-slate-600 text-lg font-medium">{{ pageContent.data.description }}</p>
</div>
<div class="bg-white p-8 rounded-xl border border-gray-200 shadow-hard space-y-6">
<section v-for="(item, index) in pageContent.data.list" :key="index">
<h2 class="text-2xl font-bold mb-4">{{ item.heading }}</h2>
<p class="leading-relaxed">{{ item.text }}</p>
</section>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {useHead} from "@unhead/vue";
const title = "Privacy Policy - Ecostream";
const description = "Read about Ecostream's commitment to protecting your privacy and data security.";
const pageContent = {
head: {
title,
meta: [
{ name: "description", content: description },
{ property: "og:title", content: title },
{ property: "og:description", content: description },
{ property: "twitter:title", content: title },
{ property: "twitter:description", content: description },
{ property: "twitter:image", content: "https://Ecostream.com/thumb.png" }
]
},
data: {
pageHeading: "Legal & Privacy Policy",
pageSubheading: "Legal & Privacy Policy",
description: "Our legal and privacy policy.",
list: [{
heading: "1. Privacy Policy",
text: "At Ecostream, we take your privacy seriously. This policy describes how we collect, use, and protect your personal information. We only collect information that is necessary for the operation of our service, including email addresses for account creation and payment information for subscription processing."
},
{
heading: "2. Data Collection",
text: "We collect data such as IP addresses, browser types, and access times to analyze trends and improve our service. Uploaded content is stored securely and is only accessed as required for the delivery of our hosting services."
},
{
heading: "3. Cookie Policy",
text: "We use cookies to maintain user sessions and preferences. By using our website, you consent to the use of cookies in accordance with this policy."
},
{
heading: "4. DMCA & Copyright",
text: "Ecostream respects the intellectual property rights of others. We respond to notices of alleged copyright infringement in accordance with the Digital Millennium Copyright Act (DMCA). Please report any copyright violations to our support team."
}]
}
}
useHead(pageContent.head);
</script>

View File

@@ -0,0 +1,67 @@
<template>
<div class="max-w-4xl mx-auto space-y-10" style="opacity: 1; transform: none;">
<div class="grow pt-32 pb-12 px-4">
<div class="max-w-4xl mx-auto space-y-10">
<div class="space-y-3">
<p
class="inline-block px-4 py-1.5 rounded-full bg-info/20 font-bold text-sm uppercase">
{{ pageContent.data.pageSubheading }}</p>
<h1 class="text-4xl md:text-5xl font-heading font-extrabold">{{ pageContent.data.pageHeading }}</h1>
<p class="text-slate-600 text-lg font-medium">{{ pageContent.data.description }}</p>
</div>
<div class="bg-white p-8 rounded-xl border border-gray-200 shadow-hard space-y-6">
<section v-for="(item, index) in pageContent.data.list" :key="index">
<h2 class="text-2xl font-bold mb-4">{{ item.heading }}</h2>
<p class="leading-relaxed">{{ item.text }}</p>
</section>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {useHead} from "@unhead/vue";
const title = "Terms and Conditions - Ecostream";
const description = "Read Ecostream's terms and conditions for using our video hosting and streaming services.";
const pageContent = {
head: {
title,
meta: [
{ name: "description", content: description },
{ property: "og:title", content: title },
{ property: "og:description", content: description },
{ property: "twitter:title", content: title },
{ property: "twitter:description", content: description },
{ property: "twitter:image", content: "https://Ecostream.com/thumb.png" }
]
},
data: {
pageHeading: "Terms and Conditions Details",
pageSubheading: "Terms and Conditions",
description: "Our terms and conditions set forth important guidelines and rules for using Ecostream's services.",
list: [
{
heading: "1. Acceptance of Terms",
text: "By accessing and using Ecostream, you accept and agree to be bound by the terms and provision of this agreement."
},
{
heading: "2. Service Usage",
text: "You agree to use our service only for lawful purposes. You are prohibited from posting or transmitting any unlawful, threatening, libelous, defamatory, obscene, or profane material. We reserve the right to terminate accounts that violate these terms."
},
{
heading: "3. Content Ownership",
text: "You retain all rights and ownership of the content you upload to Ecostream. However, by uploading content, you grant us a license to host, store, and display the content as necessary to provide our services."
},
{
heading: "4. Limitation of Liability",
text: "Ecostream shall not be liable for any direct, indirect, incidental, special, or consequential damages resulting from the use or inability to use our service."
},
{
heading: "5. Changes to Terms",
text: "We reserve the right to modify these terms at any time. Your continued use of the service after any such changes constitutes your acceptance of the new terms."
}
]
}
}
useHead(pageContent.head);
</script>

1
src/server/grpc/index.ts Normal file
View File

@@ -0,0 +1 @@
export {}

View File

@@ -0,0 +1 @@
export {}

View File

@@ -0,0 +1,56 @@
import Aura from '@primeuix/themes/aura';
import { createHead as CSRHead } from "@unhead/vue/client";
import { createHead as SSRHead } from "@unhead/vue/server";
import { createPinia } from "pinia";
import PrimeVue from 'primevue/config';
import ToastService from 'primevue/toastservice';
import Tooltip from 'primevue/tooltip';
import { createSSRApp } from 'vue';
import { RouterView } from 'vue-router';
import { withErrorBoundary } from '@/lib/hoc/withErrorBoundary';
import { vueSWR } from '@/lib/swr/use-swrv';
import createAppRouter from '@/routes';
const bodyClass = ":uno: font-sans text-gray-800 antialiased flex flex-col min-h-screen bg-gray-50";
function createApp() {
const pinia = createPinia();
const app = createSSRApp(withErrorBoundary(RouterView));
const head = import.meta.env.SSR ? SSRHead() : CSRHead();
app.use(head);
app.use(PrimeVue, {
// unstyled: true,
theme: {
preset: Aura,
options: {
darkModeSelector: '.my-app-dark',
cssLayer: false,
// cssLayer: {
// name: 'primevue',
// order: 'theme, base, primevue'
// }
}
}
});
app.use(ToastService);
app.directive('nh', {
created(el) {
el.__v_skip = true;
}
});
app.directive("tooltip", Tooltip)
if (!import.meta.env.SSR) {
Object.entries(JSON.parse(document.getElementById("__APP_DATA__")?.innerText || "{}")).forEach(([key, value]) => {
(window as any)[key] = value;
});
if ((window as any).$p ) {
pinia.state.value = (window as any).$p;
}
}
app.use(pinia);
app.use(vueSWR({revalidateOnFocus: false}));
const router = createAppRouter();
app.use(router);
return { app, router, head, pinia, bodyClass };
}
export default createApp;

View File

@@ -1,56 +0,0 @@
/**
* @module
* html Helper for Hono.
*/
import { escapeToBuffer, raw, resolveCallbackSync, stringBufferToString } from 'hono/utils/html'
import type { HtmlEscaped, HtmlEscapedString, StringBufferWithCallbacks } from 'hono/utils/html'
export const html = (
strings: TemplateStringsArray,
...values: unknown[]
): HtmlEscapedString | Promise<HtmlEscapedString> => {
const buffer: StringBufferWithCallbacks = [''] as StringBufferWithCallbacks
for (let i = 0, len = strings.length - 1; i < len; i++) {
buffer[0] += strings[i]
const children = Array.isArray(values[i])
? (values[i] as Array<unknown>).flat(Infinity)
: [values[i]]
for (let i = 0, len = children.length; i < len; i++) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const child = children[i] as any
if (typeof child === 'string') {
escapeToBuffer(child, buffer)
} else if (typeof child === 'number') {
;(buffer[0] as string) += child
} else if (typeof child === 'boolean' || child === null || child === undefined) {
continue
} else if (typeof child === 'object' && (child as HtmlEscaped).isEscaped) {
if ((child as HtmlEscapedString).callbacks) {
buffer.unshift('', child)
} else {
const tmp = child.toString()
if (tmp instanceof Promise) {
buffer.unshift('', tmp)
} else {
buffer[0] += tmp
}
}
} else if (child instanceof Promise) {
buffer.unshift('', child)
} else {
escapeToBuffer(child.toString(), buffer)
}
}
}
buffer[0] += strings.at(-1) as string
return buffer.length === 1
? 'callbacks' in buffer
? raw(resolveCallbackSync(raw(buffer[0], buffer.callbacks)))
: raw(buffer[0])
: stringBufferToString(buffer, buffer.callbacks)
}

View File

@@ -23,9 +23,7 @@ export function renderSSRLayout(c: Context, appStream: ReadableStream) {
"head", "head",
null, null,
raw('<meta charset="UTF-8"/>'), raw('<meta charset="UTF-8"/>'),
raw( raw('<meta name="viewport" content="width=device-width, initial-scale=1.0"/>'),
'<meta name="viewport" content="width=device-width, initial-scale=1.0"/>'
),
raw('<link rel="icon" href="/favicon.ico" />'), raw('<link rel="icon" href="/favicon.ico" />'),
raw(`<base href="${new URL(c.req.url).origin}/"/>`) raw(`<base href="${new URL(c.req.url).origin}/"/>`)
), ),

95
src/worker/ssrRender.ts Normal file
View File

@@ -0,0 +1,95 @@
import createVueApp from "@/shared/createVueApp";
import { renderSSRHead } from "@unhead/vue/server";
import { Context } from "hono";
import { streamText } from "hono/streaming";
import { renderToWebStream } from "vue/server-renderer";
import { buildBootstrapScript } from "@/lib/manifest";
import { styleTags } from "@/lib/primePassthrough";
import { useAuthStore } from "@/stores/auth";
// @ts-ignore
import Base from "@primevue/core/base";
import { BlankEnv, BlankInput } from "hono/types";
const defaultNames = [
"primitive",
"semantic",
"global",
"base",
"ripple-directive",
];
export async function ssrRender(
c: Context<BlankEnv, "*", BlankInput>
): Promise<Response> {
if (c.req.method !== "GET") {
return c.json({ error: "Method not allowed" }, 405);
}
const nonce = crypto.randomUUID();
const url = new URL(c.req.url);
const { app, router, head, pinia, bodyClass } = createVueApp();
app.provide("honoContext", c);
const auth = useAuthStore();
auth.$reset();
auth.initialized = false;
await auth.init();
await router.push(url.pathname);
await router.isReady();
let usedStyles = new Set<String>();
Base.setLoadedStyleName = async (name: string) => usedStyles.add(name);
return streamText(c, async (stream) => {
c.header("Content-Type", "text/html; charset=utf-8");
c.header("Content-Encoding", "Identity");
const ctx: Record<string, any> = {};
const appStream = renderToWebStream(app, ctx);
// console.log("ctx: ", );
await stream.write("<!DOCTYPE html><html lang='en'><head>");
await stream.write("<base href='" + url.origin + "'/>");
await renderSSRHead(head).then((headString) =>
stream.write(headString.headTags.replace(/\n/g, ""))
);
// await stream.write(`<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap"rel="stylesheet"></link>`);
await stream.write('<link rel="stylesheet" href="https://rsms.me/inter/inter.css">');
await stream.write('<link rel="icon" href="/favicon.ico" />');
await stream.write('<link rel="shortcut icon" href="/favicon-32x32.png" sizes="32x32" type="image/x-icon">');
await stream.write('<link rel="icon" href="/android-chrome-192x192.png" sizes="192x192" type="image/x-icon">');
await stream.write('<link rel="icon" href="/android-chrome-512x512.png" sizes="512x512" type="image/x-icon">');
await stream.write('<link rel="apple-touch-icon" href="/apple-touch-icon.png">');
await stream.write(buildBootstrapScript());
if (usedStyles.size > 0) {
defaultNames.forEach((name) => usedStyles.add(name));
}
await Promise.all(
styleTags
.filter((tag) =>
usedStyles.has(tag.name.replace(/-(variables|style)$/, ""))
)
.map((tag) =>
stream.write(
`<style type="text/css" data-primevue-style-id="${tag.name}">${tag.value}</style>`
)
)
);
await stream.write(`</head><body class='${bodyClass}'>`);
await stream.pipe(appStream);
Object.assign(ctx, { $p: pinia.state.value });
await stream.write(
`<script type="application/json" data-ssr="true" id="__APP_DATA__" nonce="${nonce}">${htmlEscape(
JSON.stringify(ctx)
)}</script>`
);
await stream.write("</body></html>");
});
}
const ESCAPE_LOOKUP: { [match: string]: string } = {
"&": "\\u0026",
">": "\\u003e",
"<": "\\u003c",
"\u2028": "\\u2028",
"\u2029": "\\u2029",
};
const ESCAPE_REGEX = /[&><\u2028\u2029]/g;
function htmlEscape(str: string): string {
return str.replace(ESCAPE_REGEX, (match) => ESCAPE_LOOKUP[match]);
}

View File

@@ -69,6 +69,11 @@ export default defineConfig({
light: "#e2e6ea", light: "#e2e6ea",
dark: "#e2e6ea", dark: "#e2e6ea",
}, },
foreground: {
DEFAULT: "#212529",
light: "#495057",
dark: "#121212",
}
}, },
boxShadow: { boxShadow: {
"primary-box": "2px 2px 10px #aff6b8", "primary-box": "2px 2px 10px #aff6b8",
@@ -91,7 +96,13 @@ export default defineConfig({
"press-animated", "press-animated",
"transition-all duration-200 ease-[cubic-bezier(.22,1,.36,1)] active:translate-y-0 active:scale-90 active:shadow-md", "transition-all duration-200 ease-[cubic-bezier(.22,1,.36,1)] active:translate-y-0 active:scale-90 active:shadow-md",
], ],
["animate-loadingBar", ["animation", "loadingBar 1.5s linear infinite"]], {
"animate-backwards": "animate-fade-in-up delay-200 duration-500",
"animate-loading-bar": "relative overflow-hidden before:absolute before:inset-0 before:bg-gradient-to-r before:from-transparent before:via-white/50 before:to-transparent before:animate-loadingBar before:content-['']",
// "bg-grid-pattern": "bg-[url(\"data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%239C92AC' fill-opacity='0.05' fill-rule='evenodd'%3E%3Cpath d='M0 40L40 0H20L0 20M40 40V20L20 40'/%3E%3C/g%3E%3C/svg%3E\")]",
},
// ["animate-loadingBar", ["animation", "loadingBar 1.5s linear infinite"]],
// ["bg-grid-pattern"]
], ],
transformers: [transformerVariantGroup(), transformerCompileClass({ transformers: [transformerVariantGroup(), transformerCompileClass({
classPrefix: "_", classPrefix: "_",
@@ -101,8 +112,9 @@ export default defineConfig({
getCSS: (context) => { getCSS: (context) => {
return ` return `
:root { :root {
--font-sans: 'Be Vietnam Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; --font-sans: Inter var, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol;
--font-serif: 'Playfair Display', serif, 'Times New Roman', Times, serif; --font-geist-sans: "Inter", "system-ui", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
--font-geist-mono: "Roboto Mono", "SFMono-Regular", "Menlo", monospace;
} }
:focus { :focus {
outline-color: ${context.theme.colors?.primary?.active}; outline-color: ${context.theme.colors?.primary?.active};
@@ -119,7 +131,7 @@ export default defineConfig({
border-bottom: 1px solid rgba(0,0,0,0.05); border-bottom: 1px solid rgba(0,0,0,0.05);
} }
.text-gradient { .text-gradient {
background: linear-gradient(135deg, #064e3b 0%, #10b981 100%); background: linear-gradient(135deg, #064e3b 0%, #2dc76b 100%);
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
@@ -132,7 +144,9 @@ export default defineConfig({
.fade-leave-to { .fade-leave-to {
opacity: 0; opacity: 0;
} }
.bg-grid-pattern {
background-image: url(\"data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%239C92AC' fill-opacity='0.05' fill-rule='evenodd'%3E%3Cpath d='M0 40L40 0H20L0 20M40 40V20L20 40'/%3E%3C/g%3E%3C/svg%3E\");
}
`; `;
}, },
}, },

View File

@@ -1,4 +1,3 @@
import { cloudflare } from "@cloudflare/vite-plugin";
import { PrimeVueResolver } from "@primevue/auto-import-resolver"; import { PrimeVueResolver } from "@primevue/auto-import-resolver";
import vue from "@vitejs/plugin-vue"; import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx"; import vueJsx from "@vitejs/plugin-vue-jsx";
@@ -10,66 +9,66 @@ import { defineConfig } from "vite";
import ssrPlugin from "./plugins/ssrPlugin"; import ssrPlugin from "./plugins/ssrPlugin";
import { vitePluginSsrMiddleware } from "./plugins/vite-plugin-ssr-middleware"; import { vitePluginSsrMiddleware } from "./plugins/vite-plugin-ssr-middleware";
export default defineConfig((env) => { export default defineConfig((env) => {
// console.log("env:", env, import.meta.env); // console.log("env:", env, import.meta.env);
return { return {
plugins: [ plugins: [
unocss(), unocss(),
vue(), vue(),
vueJsx(), vueJsx(),
AutoImport({ AutoImport({
imports: ["vue", "vue-router", "pinia"], // Common presets imports: ["vue", "vue-router", "pinia"], // Common presets
dts: true, // Generate TypeScript declaration file dts: true, // Generate TypeScript declaration file
}), }),
Components({ Components({
dirs: ["src/components"], dirs: ["src/components"],
extensions: ["vue", "tsx"], extensions: ["vue", "tsx"],
dts: true, dts: true,
dtsTsx: true, dtsTsx: true,
directives: false, directives: false,
resolvers: [PrimeVueResolver()], resolvers: [PrimeVueResolver()],
}), }),
ssrPlugin(), ssrPlugin(),
vitePluginSsrMiddleware({ vitePluginSsrMiddleware({
entry: "src/index.tsx", entry: "src/main.ts",
preview: path.resolve("dist/server/index.js"), preview: path.resolve("dist/server/index.js"),
}) }),
// devServer({ // devServer({
// entry: 'src/index.tsx', // entry: 'src/index.tsx',
// }), // }),
// cloudflare(), // cloudflare(),
], ],
environments: { environments: {
client: { client: {
build: { build: {
outDir: "dist/client", outDir: "dist/client",
rollupOptions: { rollupOptions: {
input: { index: "/src/client.ts" }, input: { index: "/src/client.ts" },
}, },
} },
}, },
server: { server: {
build: { build: {
outDir: "dist/server", outDir: "dist/server",
copyPublicDir: false, copyPublicDir: false,
rollupOptions: { rollupOptions: {
input: { index: "/src/index.tsx" }, input: { index: "/src/main.ts" },
}, },
},
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
// "httpClientAdapter": path.resolve(__dirname, "./src/api/httpClientAdapter.server.ts")
},
},
optimizeDeps: {
exclude: ["vue"],
}, },
},
},
resolve: {
alias: {
"@": path.resolve(__dirname, "./src"),
// "httpClientAdapter": path.resolve(__dirname, "./src/api/httpClientAdapter.server.ts")
},
},
optimizeDeps: {
exclude: ["vue"],
},
ssr: { ssr: {
// external: ["vue"] // external: ["vue"]
// noExternal: ["vue"], // noExternal: ["vue"],
}, },
}; };
}); });