Add CheckIcon component and update routes, auth store, and various UI improvements

This commit is contained in:
2026-01-05 01:06:17 +07:00
parent aa9df98926
commit 22af8c4f2b
12 changed files with 155 additions and 120 deletions

2
components.d.ts vendored
View File

@@ -18,6 +18,7 @@ declare module 'vue' {
BellFilled: typeof import('./src/components/icons/BellFilled.vue')['default'] BellFilled: typeof import('./src/components/icons/BellFilled.vue')['default']
Button: typeof import('primevue/button')['default'] Button: typeof import('primevue/button')['default']
Checkbox: typeof import('primevue/checkbox')['default'] Checkbox: typeof import('primevue/checkbox')['default']
CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
Home: typeof import('./src/components/icons/Home.vue')['default'] Home: typeof import('./src/components/icons/Home.vue')['default']
HomeFilled: typeof import('./src/components/icons/HomeFilled.vue')['default'] HomeFilled: typeof import('./src/components/icons/HomeFilled.vue')['default']
@@ -42,6 +43,7 @@ declare global {
const BellFilled: typeof import('./src/components/icons/BellFilled.vue')['default'] const BellFilled: typeof import('./src/components/icons/BellFilled.vue')['default']
const Button: typeof import('primevue/button')['default'] const Button: typeof import('primevue/button')['default']
const Checkbox: typeof import('primevue/checkbox')['default'] const Checkbox: typeof import('primevue/checkbox')['default']
const CheckIcon: typeof import('./src/components/icons/CheckIcon.vue')['default']
const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default'] const DashboardLayout: typeof import('./src/components/DashboardLayout.vue')['default']
const Home: typeof import('./src/components/icons/Home.vue')['default'] const Home: typeof import('./src/components/icons/Home.vue')['default']
const HomeFilled: typeof import('./src/components/icons/HomeFilled.vue')['default'] const HomeFilled: typeof import('./src/components/icons/HomeFilled.vue')['default']

View File

@@ -164,7 +164,6 @@ const login = async (username: string, password: string) => {
}; };
async function checkAuth() { async function checkAuth() {
console.log("Check auth called");
const context = getContext<HonoVarTypes>(); const context = getContext<HonoVarTypes>();
const token = getCookie(context, 'auth_token'); const token = getCookie(context, 'auth_token');
@@ -183,7 +182,6 @@ async function checkAuth() {
if (!userRecord) { if (!userRecord) {
return { authenticated: false, user: null }; return { authenticated: false, user: null };
} }
// console.log("Check auth called 2", userRecord);
return { return {
authenticated: true, authenticated: true,

View File

@@ -1,3 +1,3 @@
<template> <template>
<router-view /> <router-view/>
</template> </template>

View File

@@ -0,0 +1,3 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="24" viewBox="0 0 532 532"><path d="M10 266c0 37 21 69 51 85-10 33-2 70 24 96s63 34 96 24c16 30 48 51 85 51s69-21 85-51c33 10 70 2 96-24s34-63 24-96c30-16 51-48 51-85s-21-69-51-85c10-33 2-70-24-96s-63-34-96-24c-16-30-48-51-85-51s-69 21-85 51c-33-10-70-2-96 24s-34 63-24 96c-30 16-51 48-51 85zm152 42c-9-10-9-25 1-34 9-9 25-9 34 0l36 37 106-145c8-11 23-14 33-6 11 8 13 23 6 34L255 363c-4 5-11 9-18 10-7 0-14-3-19-8l-56-57z" fill="#a6acb9"/><path d="M339 166c8-11 23-14 33-6 11 8 13 23 6 34L255 363c-4 5-11 9-18 10-7 0-14-3-19-8l-56-57c-9-10-9-25 1-34 9-9 25-9 34 0l36 37 106-145z" fill="#1e3050"/></svg>
</template>

View File

@@ -33,12 +33,12 @@ app.get("*", async (c) => {
const url = new URL(c.req.url); const url = new URL(c.req.url);
const { app, router, head, pinia, bodyClass } = createApp(); const { app, router, head, pinia, bodyClass } = createApp();
app.provide("honoContext", c); app.provide("honoContext", c);
await router.push(url.pathname);
await router.isReady().then(() => {
const auth = useAuthStore(); const auth = useAuthStore();
auth.$reset();
auth.initialized = false; auth.initialized = false;
auth.init(); await auth.init();
}); await router.push(url.pathname);
await router.isReady();
let usedStyles = new Set(defaultNames); let usedStyles = new Set(defaultNames);
Base.setLoadedStyleName = async (name: string) => usedStyles.add(name) Base.setLoadedStyleName = async (name: string) => usedStyles.add(name)
return streamText(c, async (stream) => { return streamText(c, async (stream) => {

View File

@@ -4,12 +4,13 @@ import { createSSRApp } from 'vue';
import { RouterView } from 'vue-router'; import { RouterView } from 'vue-router';
import { withErrorBoundary } from './lib/hoc/withErrorBoundary'; import { withErrorBoundary } from './lib/hoc/withErrorBoundary';
import { vueSWR } from './lib/swr/use-swrv'; import { vueSWR } from './lib/swr/use-swrv';
import router from './routes'; import createAppRouter from './routes';
import PrimeVue from 'primevue/config'; import PrimeVue from 'primevue/config';
import Aura from '@primeuix/themes/aura'; import Aura from '@primeuix/themes/aura';
import { createPinia } from "pinia"; import { createPinia } from "pinia";
import { useAuthStore } from './stores/auth'; import { useAuthStore } from './stores/auth';
import ToastService from 'primevue/toastservice';
import Tooltip from 'primevue/tooltip';
const bodyClass = ":uno: font-sans bg-[#f9fafd] text-gray-800 antialiased flex flex-col min-h-screen" const bodyClass = ":uno: font-sans bg-[#f9fafd] text-gray-800 antialiased flex flex-col min-h-screen"
export function createApp() { export function createApp() {
const pinia = createPinia(); const pinia = createPinia();
@@ -30,22 +31,22 @@ export function createApp() {
} }
} }
}); });
app.use(ToastService);
app.directive('no-hydrate', { app.directive('no-hydrate', {
created(el) { created(el) {
el.__v_skip = true; el.__v_skip = true;
} }
}); });
app.use(vueSWR({revalidateOnFocus: false})); app.directive("tooltip", Tooltip)
app.use(router);
app.use(pinia);
// Initialize auth store on client side
if (!import.meta.env.SSR) { if (!import.meta.env.SSR) {
router.isReady().then(() => { if ((window as any).__PINIA_STATE__ ) {
const auth = useAuthStore(); pinia.state.value = (window as any).__PINIA_STATE__;
auth.init();
});
} }
}
app.use(pinia);
app.use(vueSWR({revalidateOnFocus: false}));
const router = createAppRouter();
app.use(router);
return { app, router, head, pinia, bodyClass }; return { app, router, head, pinia, bodyClass };
} }

View File

@@ -1,15 +1,18 @@
<template> <template>
<div class="w-full max-w-md bg-white p-8 rounded-xl border border-primary m-auto"> <div class="w-full max-w-md bg-white p-8 rounded-xl border border-primary m-auto overflow-hidden">
<div class="text-center mb-8"> <div class="text-center mb-8">
<div 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" />
</div> </router-link>
<h2 class="text-2xl font-bold text-gray-900"> <h2 class="text-2xl font-bold text-gray-900">
{{ content[route.name as keyof typeof content]?.title || '' }} {{ content[route.name as keyof typeof content]?.title || '' }}
</h2> </h2>
<p class="text-gray-500 text-sm mt-1"> <p class="text-gray-500 text-sm mt-1">
{{ content[route.name as keyof typeof content]?.subtitle || '' }} {{ content[route.name as keyof typeof content]?.subtitle || '' }}
</p> </p>
<vue-head :input="{
title: content[route.name as keyof typeof content]?.headTitle || 'Authentication'
}" />
</div> </div>
<router-view /> <router-view />
</div> </div>
@@ -20,16 +23,19 @@ import { useRoute } from 'vue-router';
const route = useRoute(); const route = useRoute();
const content = { const content = {
login: { login: {
headTitle: "Login to your account",
title: 'Welcome back', title: 'Welcome back',
subtitle: 'Please enter your details to sign in.' subtitle: 'Please enter your details to sign in.'
}, },
signup: { signup: {
headTitle: "Create your account",
title: 'Create your account', title: 'Create your account',
subtitle: 'Please fill in the information to create your account.' subtitle: 'Please fill in the information to create your account.'
}, },
forgot: { forgot: {
title: 'Forgot your password?', title: 'Forgot your password?',
subtitle: "Enter your email address and we'll send you a link to reset your password." subtitle: "Enter your email address and we'll send you a link to reset your password.",
headTitle: "Reset your password"
} }
} }
</script> </script>

View File

@@ -1,13 +1,8 @@
<template> <template>
<div class="w-full"> <div class="w-full">
<Toast />
<Form v-slot="$form" :resolver="resolver" :initialValues="initialValues" @submit="onFormSubmit" <Form v-slot="$form" :resolver="resolver" :initialValues="initialValues" @submit="onFormSubmit"
class="flex flex-col gap-4 w-full"> class="flex flex-col gap-4 w-full">
<!-- Global error message -->
<Message v-if="auth.error" severity="error" size="small" variant="simple">
{{ auth.error }}
</Message>
<div class="flex flex-col gap-1"> <div class="flex flex-col gap-1">
<label for="email" class="text-sm font-medium text-gray-700">Email or Username</label> <label for="email" class="text-sm font-medium text-gray-700">Email or Username</label>
<InputText name="email" type="text" placeholder="admin or user@example.com" fluid <InputText name="email" type="text" placeholder="admin or user@example.com" fluid
@@ -58,7 +53,7 @@
<p class="mt-4 text-center text-sm text-gray-600"> <p class="mt-4 text-center text-sm text-gray-600">
Don't have an account? Don't have an account?
<router-link to="/signup" class="font-medium text-blue-600 hover:text-blue-500 hover:underline">Sign up <router-link to="/sign-up" class="font-medium text-blue-600 hover:text-blue-500 hover:underline">Sign up
for free</router-link> for free</router-link>
</p> </p>
@@ -80,8 +75,16 @@ import { Form, type FormSubmitEvent } from '@primevue/forms';
import { zodResolver } from '@primevue/forms/resolvers/zod'; import { zodResolver } from '@primevue/forms/resolvers/zod';
import { z } from 'zod'; import { z } from 'zod';
import { useAuthStore } from '@/stores/auth'; import { useAuthStore } from '@/stores/auth';
import Toast from 'primevue/toast';
import { useToast } from "primevue/usetoast";
const t = useToast();
const auth = useAuthStore(); const auth = useAuthStore();
// const $form = Form.useFormContext();
watch(() => auth.error, (newError) => {
if (newError) {
t.add({ severity: 'error', summary: String(auth.error), detail: newError, life: 5000 });
}
});
const initialValues = reactive({ const initialValues = reactive({
email: '', email: '',
@@ -97,14 +100,7 @@ const resolver = zodResolver(
); );
const onFormSubmit = async ({ valid, values }: FormSubmitEvent) => { const onFormSubmit = async ({ valid, values }: FormSubmitEvent) => {
if (valid) { if (valid) auth.login(values.email, values.password);
try {
await auth.login(values.email, values.password);
} catch (error) {
// Error is already set in the store
console.error('Login failed:', error);
}
}
}; };
const loginWithGoogle = () => { const loginWithGoogle = () => {

View File

@@ -2,23 +2,16 @@
<nav class="fixed w-full z-50 glass-nav transition-all duration-300" id="navbar"> <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="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 justify-between h-16">
<!-- Logo --> <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" />
<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> <span class="font-bold text-xl tracking-tight text-slate-900">EcoStream</span>
</div> </div>
<!-- Desktop Menu -->
<div class="hidden md:flex items-center space-x-8"> <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="#features" class="text-sm font-medium text-slate-600 hover:text-brand-600 transition-colors">Features</a>
<a href="#analytics" class="text-sm font-medium text-slate-600 hover:text-brand-600 transition-colors">Analytics</a>
<a href="#pricing" class="text-sm font-medium text-slate-600 hover:text-brand-600 transition-colors">Pricing</a> <a href="#pricing" class="text-sm font-medium text-slate-600 hover:text-brand-600 transition-colors">Pricing</a>
</div> </div>
<!-- Auth Buttons -->
<div class="hidden md:flex items-center gap-4"> <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="/login" class="text-sm font-semibold text-slate-600 hover:text-slate-900 cursor-pointer">Log in</RouterLink>
<RouterLink to="/signup" class="bg-slate-900 hover:bg-black text-white px-5 py-2.5 rounded-lg text-sm font-semibold cursor-pointer"> <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 Start for free
</RouterLink> </RouterLink>
</div> </div>
@@ -26,7 +19,6 @@
</div> </div>
</nav> </nav>
<section class="relative pt-32 pb-20 lg:pt-48 lg:pb-32 overflow-hidden"> <section class="relative pt-32 pb-20 lg:pt-48 lg:pb-32 overflow-hidden">
<!-- Background Elements -->
<div class="absolute inset-0 opacity-[0.4] -z-10"></div> <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 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="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>
@@ -62,24 +54,19 @@
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6"> <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
<!-- Large Feature -->
<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="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="relative z-10">
<div class="w-12 h-12 bg-white rounded-xl flex items-center justify-center mb-6 border border-slate-100"> <div class="w-12 h-12 bg-white rounded-xl flex items-center justify-center mb-6 border border-slate-100">
<!-- <i class="fas fa-globe text-xl"></i> -->
<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> <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> </div>
<h3 class="text-xl font-bold text-slate-900 mb-2">Global Edge Network</h3> <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> <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>
<!-- Decor -->
<div class="absolute right-0 bottom-0 opacity-10 translate-x-1/4 translate-y-1/4"> <div class="absolute right-0 bottom-0 opacity-10 translate-x-1/4 translate-y-1/4">
<!-- <i class="fas fa-globe-americas text-[200px] text-brand-900"></i> -->
<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> <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> </div>
<!-- Tall Feature -->
<div class="md:row-span-2 bg-slate-900 rounded-2xl p-8 text-white relative overflow-hidden group"> <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="absolute inset-0 bg-gradient-to-b from-slate-800/50 to-transparent"></div>
<div class="relative z-10"> <div class="relative z-10">
@@ -125,60 +112,27 @@
</div> </div>
</section> </section>
<!-- Pricing --> <!-- Pricing -->
<section id="pricing" class="py-24 bg-white border-t border-slate-100"> <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="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="text-center mb-16"> <div class="text-center mb-16">
<h2 class="text-3xl font-bold text-slate-900 mb-4">Simple, transparent pricing</h2> <h2 class="text-3xl font-bold text-slate-900 mb-4">{{ pricing.title }}</h2>
<p class="text-slate-500">No hidden fees. Pay as you grow.</p> <p class="text-slate-500">{{ pricing.subtitle }}</p>
</div> </div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 max-w-5xl mx-auto"> <div class="grid grid-cols-1 md:grid-cols-3 gap-8 w-full">
<!-- Hobby --> <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 class="p-8 rounded-2xl border border-slate-200 hover:border-slate-300 transition-colors"> <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>
<h3 class="font-semibold text-slate-900 mb-2">Hobby</h3> <div>
<h3 class="font-semibold text-slate-900 text-xl mb-2">{{ pack.name }}</h3>
<div class="flex items-baseline gap-1 mb-6"> <div class="flex items-baseline gap-1 mb-6">
<span class="text-4xl font-bold text-slate-900">$0</span> <span class="text-4xl font-bold text-slate-900">{{ pack.price }}</span>
<span class="text-slate-500">/mo</span> <span class="text-slate-500">/mo</span>
</div> </div>
<ul class="space-y-3 mb-8 text-sm text-slate-600">
<li class="flex items-center gap-3"><i class="fas fa-check text-brand-500"></i> 100 GB Bandwidth</li>
<li class="flex items-center gap-3"><i class="fas fa-check text-brand-500"></i> 1 Hour of Storage</li>
<li class="flex items-center gap-3"><i class="fas fa-check text-brand-500"></i> Standard Support</li>
</ul>
<button class="w-full py-2.5 rounded-lg border border-slate-200 font-semibold text-slate-700 hover:bg-slate-50 transition-colors">Start Free</button>
</div>
<!-- Pro -->
<div class="p-8 rounded-2xl bg-slate-900 text-white shadow-2xl relative overflow-hidden transform">
<div class="absolute top-0 right-0 bg-primary/50 text-white text-xs font-bold px-3 py-1 rounded-bl-lg">POPULAR</div>
<h3 class="font-semibold mb-2 text-brand-400">Pro</h3>
<div class="flex items-baseline gap-1 mb-6">
<span class="text-4xl font-bold">$0</span>
<span class="text-lg font-bold line-through">$29</span>
<span class="text-slate-400">/mo</span>
</div>
<ul class="space-y-3 mb-8 text-sm text-slate-300">
<li class="flex items-center gap-3"><i class="fas fa-check text-primary/60"></i> 1 TB Bandwidth</li>
<li class="flex items-center gap-3"><i class="fas fa-check text-primary/60"></i> 100 Hours Storage</li>
<li class="flex items-center gap-3"><i class="fas fa-check text-primary/60"></i> Remove Branding</li>
<li class="flex items-center gap-3"><i class="fas fa-check text-primary/60"></i> 4K Encoding</li>
</ul>
<button class="w-full py-2.5 rounded-lg bg-primary/60 hover:bg-primary/70 font-semibold transition-colors shadow-lg shadow-primary/30">Get Started</button>
</div>
<!-- Scale -->
<div class="p-8 rounded-2xl border border-slate-200 hover:border-slate-300 transition-colors">
<h3 class="font-semibold text-slate-900 mb-2">Scale</h3>
<div class="flex items-baseline gap-1 mb-6">
<span class="text-4xl font-bold text-slate-900">$99</span>
<span class="text-slate-500">/mo</span>
</div> </div>
<ul class="space-y-3 mb-8 text-sm text-slate-600"> <ul class="space-y-3 mb-8 text-sm text-slate-600">
<li class="flex items-center gap-3"><i class="fas fa-check text-brand-500"></i> 5 TB Bandwidth</li> <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>
<li class="flex items-center gap-3"><i class="fas fa-check text-brand-500"></i> 500 Hours Storage</li>
<li class="flex items-center gap-3"><i class="fas fa-check text-brand-500"></i> Priority Support</li>
</ul> </ul>
<button class="w-full py-2.5 rounded-lg border border-slate-200 font-semibold text-slate-700 hover:bg-slate-50 transition-colors">Contact Sales</button> <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> </div>
</div> </div>
@@ -226,6 +180,55 @@
</div> </div>
</div> </div>
</footer> </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> </template>
<script lang="ts" setup> <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> </script>

View File

@@ -38,7 +38,7 @@ const routes: RouteData[] = [
component: () => import("./auth/login.vue"), component: () => import("./auth/login.vue"),
}, },
{ {
path: "signup", path: "sign-up",
name: "signup", name: "signup",
component: () => import("./auth/signup.vue"), component: () => import("./auth/signup.vue"),
}, },
@@ -84,6 +84,7 @@ const routes: RouteData[] = [
], ],
}, },
]; ];
const createAppRouter = () => {
const router = createRouter({ const router = createRouter({
history: import.meta.env.SSR history: import.meta.env.SSR
? createMemoryHistory() // server ? createMemoryHistory() // server
@@ -93,7 +94,6 @@ const router = createRouter({
router.beforeEach((to, from, next) => { router.beforeEach((to, from, next) => {
const auth = useAuthStore(); const auth = useAuthStore();
console.log("Call on server:", Math.random());
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" });
@@ -104,5 +104,7 @@ router.beforeEach((to, from, next) => {
next(); next();
} }
}); });
return router;
}
export default router; export default createAppRouter;

View File

@@ -20,15 +20,10 @@ export const useAuthStore = defineStore('auth', () => {
// Check auth status on init (reads from cookie) // Check auth status on init (reads from cookie)
async function init() { async function init() {
console.log("Auth store init called"); if (initialized.value) return;
// if (initialized.value) return;
try { try {
const response = await client.checkAuth().then((res) => { const response = await client.checkAuth();
console.log("call", res);
return res;
});
if (response.authenticated && response.user) { if (response.authenticated && response.user) {
user.value = response.user; user.value = response.user;
// Get CSRF token if authenticated // Get CSRF token if authenticated
@@ -49,18 +44,30 @@ export const useAuthStore = defineStore('auth', () => {
async function login(username: string, password: string) { async function login(username: string, password: string) {
loading.value = true; loading.value = true;
error.value = null; error.value = null;
try { return client.login(username, password).then((response) => {
const response = await client.login(username, password);
user.value = response.user; user.value = response.user;
csrfToken.value = response.csrfToken; csrfToken.value = response.csrfToken;
router.push('/'); router.push('/');
} catch (e: any) { }).catch((e: any) => {
console.log(JSON.parse(e.message)) // error.value = e.message || 'Login failed';
error.value = e.message || 'Login failed'; error.value = 'Login failed';
throw e; throw e;
} finally { }).finally(() => {
loading.value = false; loading.value = false;
} });
// try {
// const response = await client.login(username, password);
// user.value = response.user;
// csrfToken.value = response.csrfToken;
// router.push('/');
// } catch (e: any) {
// // console.log(JSON.parse(e.message))
// // error.value = e.message || 'Login failed';
// error.value = 'Login failed';
// throw e;
// } finally {
// loading.value = false;
// }
} }
async function register(username: string, email: string, password: string) { async function register(username: string, email: string, password: string) {
@@ -72,7 +79,8 @@ export const useAuthStore = defineStore('auth', () => {
csrfToken.value = response.csrfToken; csrfToken.value = response.csrfToken;
router.push('/'); router.push('/');
} catch (e: any) { } catch (e: any) {
error.value = e.message || 'Registration failed'; // error.value = e.message || 'Registration failed';
error.value = 'Registration failed';
throw e; throw e;
} finally { } finally {
loading.value = false; loading.value = false;
@@ -90,5 +98,11 @@ export const useAuthStore = defineStore('auth', () => {
router.push('/'); router.push('/');
} }
return { user, loading, error, csrfToken, initialized, init, login, register, logout }; return { user, loading, error, csrfToken, initialized, init, login, register, logout, $reset: () => {
user.value = null;
loading.value = false;
error.value = null;
csrfToken.value = null;
initialized.value = false;
} };
}); });

View File

@@ -121,6 +121,16 @@ export default defineConfig({
-webkit-background-clip: text; -webkit-background-clip: text;
-webkit-text-fill-color: transparent; -webkit-text-fill-color: transparent;
} }
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
`; `;
}, },
}, },