tr]:last:border-b-0",
48 | className
49 | )}
50 | {...props}
51 | />
52 | );
53 | }
54 |
55 | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
56 | return (
57 |
65 | );
66 | }
67 |
68 | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
69 | return (
70 | [role=checkbox]]:translate-y-[2px]",
74 | className
75 | )}
76 | {...props}
77 | />
78 | );
79 | }
80 |
81 | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
82 | return (
83 | | [role=checkbox]]:translate-y-[2px]",
87 | className
88 | )}
89 | {...props}
90 | />
91 | );
92 | }
93 |
94 | function TableCaption({
95 | className,
96 | ...props
97 | }: React.ComponentProps<"caption">) {
98 | return (
99 |
104 | );
105 | }
106 |
107 | export {
108 | Table,
109 | TableHeader,
110 | TableBody,
111 | TableFooter,
112 | TableHead,
113 | TableRow,
114 | TableCell,
115 | TableCaption,
116 | };
117 |
--------------------------------------------------------------------------------
/packages/ui/src/components/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area";
5 |
6 | import { cn } from "@neostack/ui/lib/utils"; // Make sure this path is correct for your project
7 |
8 | // Define an interface for the props, including the new viewportRef
9 | interface ScrollAreaProps
10 | extends React.ComponentPropsWithoutRef {
11 | /** Optional ref to be passed to the ScrollAreaPrimitive.Viewport element. */
12 | viewportRef?: React.Ref;
13 | }
14 |
15 | const ScrollArea = React.forwardRef<
16 | // Type of the element the main ref points to (the Root)
17 | React.ElementRef,
18 | // Type of the props the component accepts
19 | ScrollAreaProps
20 | >(({ className, children, viewportRef, ...props }, ref) => (
21 |
28 |
34 | {children}
35 |
36 |
37 |
38 |
39 | ));
40 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; // For better debugging
41 |
42 | // ScrollBar using forwardRef as well (good practice)
43 | const ScrollBar = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, orientation = "vertical", ...props }, ref) => (
47 |
63 |
68 |
69 | ));
70 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; // For better debugging
71 |
72 | export { ScrollArea, ScrollBar };
73 |
--------------------------------------------------------------------------------
/packages/ui/src/components/calendar.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { ChevronLeft, ChevronRight } from "lucide-react";
5 | import { DayPicker } from "react-day-picker";
6 |
7 | import { cn } from "@neostack/ui/lib/utils";
8 | import { buttonVariants } from "@neostack/ui/components/button";
9 |
10 | function Calendar({
11 | className,
12 | classNames,
13 | showOutsideDays = true,
14 | ...props
15 | }: React.ComponentProps) {
16 | return (
17 | .day-range-end)]:rounded-r-md [&:has(>.day-range-start)]:rounded-l-md first:[&:has([aria-selected])]:rounded-l-md last:[&:has([aria-selected])]:rounded-r-md"
41 | : "[&:has([aria-selected])]:rounded-md"
42 | ),
43 | day: cn(
44 | buttonVariants({ variant: "ghost" }),
45 | "size-8 p-0 font-normal aria-selected:opacity-100"
46 | ),
47 | day_range_start:
48 | "day-range-start aria-selected:bg-primary aria-selected:text-primary-foreground",
49 | day_range_end:
50 | "day-range-end aria-selected:bg-primary aria-selected:text-primary-foreground",
51 | day_selected:
52 | "bg-primary text-primary-foreground hover:bg-primary hover:text-primary-foreground focus:bg-primary focus:text-primary-foreground",
53 | day_today: "bg-accent text-accent-foreground",
54 | day_outside:
55 | "day-outside text-muted-foreground aria-selected:text-muted-foreground",
56 | day_disabled: "text-muted-foreground opacity-50",
57 | day_range_middle:
58 | "aria-selected:bg-accent aria-selected:text-accent-foreground",
59 | day_hidden: "invisible",
60 | ...classNames,
61 | }}
62 | components={{
63 | IconLeft: ({ className, ...props }) => (
64 |
65 | ),
66 | IconRight: ({ className, ...props }) => (
67 |
68 | ),
69 | }}
70 | {...props}
71 | />
72 | );
73 | }
74 |
75 | export { Calendar };
76 |
--------------------------------------------------------------------------------
/apps/api/src/lib/auth.ts:
--------------------------------------------------------------------------------
1 | // auth.ts
2 | import { betterAuth } from "better-auth";
3 | import { drizzleAdapter } from "better-auth/adapters/drizzle";
4 | import { stripe } from "@better-auth/stripe";
5 | import {
6 | oneTimeToken,
7 | admin,
8 | haveIBeenPwned,
9 | openAPI,
10 | organization,
11 | } from "better-auth/plugins";
12 | import Stripe from "stripe";
13 | import { schema } from "@neostack/database";
14 | import { drizzle } from "@neostack/database";
15 | import {
16 | sendResetPasswordEmail,
17 | sendVerificationEmail,
18 | } from "./auth/hooks/emails";
19 | import { AppEnv } from "@/types/AppEnv";
20 |
21 | export const createAuthClient = (
22 | env: AppEnv
23 | ): ReturnType => {
24 | return betterAuth({
25 | appName: "NeoStack",
26 | baseURL: `${env.BETTER_AUTH_URL}/v1/auth`,
27 | basePath: "/v1/auth",
28 | trustedOrigins: env.TRUSTED_ORIGINS.split(","),
29 |
30 | advanced: {
31 | cookiePrefix: "neostack",
32 | crossSubDomainCookies: {
33 | enabled: true,
34 | domain: `.${env.BETTER_AUTH_COOKIE_DOMAIN}`,
35 | },
36 | },
37 |
38 | database: drizzleAdapter(drizzle(env.HYPERDRIVE.connectionString), {
39 | provider: "pg",
40 | usePlural: true,
41 | schema,
42 | }),
43 |
44 | emailAndPassword: {
45 | enabled: true,
46 | requireEmailVerification: true,
47 | sendResetPassword: async ({ user, url, token }, request) => {
48 | await sendResetPasswordEmail(env, user, url);
49 | },
50 | },
51 | emailVerification: {
52 | autoSignInAfterVerification: true,
53 | sendVerificationEmail: async ({ user, url }) => {
54 | await sendVerificationEmail(env, user, url);
55 | },
56 | },
57 | socialProviders: {
58 | google: {
59 | clientId: env.GOOGLE_CLIENT_ID,
60 | clientSecret: env.GOOGLE_CLIENT_SECRET,
61 | },
62 | },
63 | plugins: [
64 | stripe({
65 | stripeClient: new Stripe(env.STRIPE_SECRET_KEY),
66 | stripeWebhookSecret: env.STRIPE_WEBHOOK_SECRET,
67 | createCustomerOnSignUp: true,
68 | onCustomerCreate: async (
69 | { customer, stripeCustomer, user },
70 | request
71 | ) => {
72 | console.log(`Customer ${customer.id} created for user ${user.id}`);
73 | },
74 | subscription: {
75 | enabled: true,
76 | plans: [],
77 | },
78 | }),
79 | openAPI(),
80 | haveIBeenPwned(),
81 | admin(),
82 | organization(),
83 | ],
84 | });
85 | };
86 |
87 | export const auth: any = betterAuth({
88 | database: drizzleAdapter(
89 | drizzle("postgres://john:doe@localhost:5432/postgres"),
90 | {
91 | provider: "pg",
92 | usePlural: true,
93 | schema,
94 | }
95 | ),
96 | emailAndPassword: {
97 | enabled: true,
98 | },
99 |
100 | plugins: [
101 | stripe({
102 | stripeClient: new Stripe("dummy-key"),
103 | stripeWebhookSecret: "dummy-key",
104 | createCustomerOnSignUp: true,
105 |
106 | subscription: {
107 | enabled: true,
108 | plans: [],
109 | },
110 | }),
111 | openAPI(),
112 | haveIBeenPwned(),
113 | oneTimeToken(),
114 | admin(),
115 | organization(),
116 | ],
117 | });
118 |
--------------------------------------------------------------------------------
/packages/ui/src/components/pagination.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import {
3 | ChevronLeftIcon,
4 | ChevronRightIcon,
5 | MoreHorizontalIcon,
6 | } from "lucide-react";
7 |
8 | import { cn } from "@neostack/ui/lib/utils";
9 | import { Button, buttonVariants } from "@neostack/ui/components/button";
10 |
11 | function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
12 | return (
13 |
20 | );
21 | }
22 |
23 | function PaginationContent({
24 | className,
25 | ...props
26 | }: React.ComponentProps<"ul">) {
27 | return (
28 |
33 | );
34 | }
35 |
36 | function PaginationItem({ ...props }: React.ComponentProps<"li">) {
37 | return ;
38 | }
39 |
40 | type PaginationLinkProps = {
41 | isActive?: boolean;
42 | } & Pick, "size"> &
43 | React.ComponentProps<"a">;
44 |
45 | function PaginationLink({
46 | className,
47 | isActive,
48 | size = "icon",
49 | ...props
50 | }: PaginationLinkProps) {
51 | return (
52 |
65 | );
66 | }
67 |
68 | function PaginationPrevious({
69 | className,
70 | ...props
71 | }: React.ComponentProps) {
72 | return (
73 |
79 |
80 | Previous
81 |
82 | );
83 | }
84 |
85 | function PaginationNext({
86 | className,
87 | ...props
88 | }: React.ComponentProps) {
89 | return (
90 |
96 | Next
97 |
98 |
99 | );
100 | }
101 |
102 | function PaginationEllipsis({
103 | className,
104 | ...props
105 | }: React.ComponentProps<"span">) {
106 | return (
107 |
113 |
114 | More pages
115 |
116 | );
117 | }
118 |
119 | export {
120 | Pagination,
121 | PaginationContent,
122 | PaginationLink,
123 | PaginationItem,
124 | PaginationPrevious,
125 | PaginationNext,
126 | PaginationEllipsis,
127 | };
128 |
--------------------------------------------------------------------------------
/packages/constants/src/tailwindConfig.ts:
--------------------------------------------------------------------------------
1 | export const tailwindConfig = {
2 | theme: {
3 | extend: {
4 | colors: {
5 | background: "#F7F5E6",
6 | foreground: "#4A4E42",
7 | card: "#F7F5E6",
8 | "card-foreground": "#2E2F2B",
9 | popover: "#FFFFFF",
10 | "popover-foreground": "#3C3F36",
11 | primary: "#C75000",
12 | "primary-foreground": "#FFFFFF",
13 | secondary: "#E8E4CF",
14 | "secondary-foreground": "#6B695C",
15 | muted: "#EBE7C6",
16 | "muted-foreground": "#9C9986",
17 | accent: "#E8E4CF",
18 | "accent-foreground": "#3C3F36",
19 | destructive: "#8B1D07",
20 | "destructive-foreground": "#FFDBD2",
21 | border: "#DEDAC3",
22 | input: "#BDBA9E",
23 | ring: "#4E5AD4",
24 | "chart-1": "#A94408",
25 | "chart-2": "#A135BF",
26 | "chart-3": "#DED9B8",
27 | "chart-4": "#D9D2E0",
28 | "chart-5": "#A64509",
29 | sidebar: "#F5F3DD",
30 | "sidebar-foreground": "#56584D",
31 | "sidebar-primary": "#C75000",
32 | "sidebar-primary-foreground": "#FCFCFC",
33 | "sidebar-accent": "#E8E4CF",
34 | "sidebar-accent-foreground": "#535353",
35 | "sidebar-border": "#EFEFEF",
36 | "sidebar-ring": "#C5C5C5",
37 | "shadow-color": "#000000",
38 | },
39 | fontFamily: {
40 | sans: [
41 | "ui-sans-serif",
42 | "system-ui",
43 | "-apple-system",
44 | "BlinkMacSystemFont",
45 | '"Segoe UI"',
46 | "Roboto",
47 | '"Helvetica Neue"',
48 | "Arial",
49 | '"Noto Sans"',
50 | "sans-serif",
51 | '"Apple Color Emoji"',
52 | '"Segoe UI Emoji"',
53 | '"Segoe UI Symbol"',
54 | '"Noto Color Emoji"',
55 | ],
56 | serif: [
57 | "ui-serif",
58 | "Georgia",
59 | "Cambria",
60 | '"Times New Roman"',
61 | "Times",
62 | "serif",
63 | ],
64 | mono: [
65 | "ui-monospace",
66 | "SFMono-Regular",
67 | "Menlo",
68 | "Monaco",
69 | "Consolas",
70 | '"Liberation Mono"',
71 | '"Courier New"',
72 | "monospace",
73 | ],
74 | },
75 | borderRadius: {
76 | DEFAULT: "0.5rem",
77 | sm: "0.25rem",
78 | md: "0.375rem",
79 | lg: "0.5rem",
80 | xl: "0.75rem",
81 | },
82 | spacing: {
83 | DEFAULT: "0.25rem",
84 | },
85 | letterSpacing: {
86 | normal: "0em",
87 | tighter: "-0.05em",
88 | tight: "-0.025em",
89 | wide: "0.025em",
90 | wider: "0.05em",
91 | widest: "0.1em",
92 | },
93 | boxShadow: {
94 | "2xs": "0 1px 3px 0 rgba(0, 0, 0, 0.05)",
95 | xs: "0 1px 3px 0 rgba(0, 0, 0, 0.05)",
96 | sm: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)",
97 | DEFAULT: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px -1px rgba(0, 0, 0, 0.1)",
98 | md: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.1)",
99 | lg: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 4px 6px -1px rgba(0, 0, 0, 0.1)",
100 | xl: "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 8px 10px -1px rgba(0, 0, 0, 0.1)",
101 | "2xl": "0 1px 3px 0 rgba(0, 0, 0, 0.25)",
102 | },
103 | },
104 | },
105 | };
--------------------------------------------------------------------------------
/packages/database/src/schema/auth.ts:
--------------------------------------------------------------------------------
1 | import {
2 | pgTable,
3 | text,
4 | timestamp,
5 | boolean,
6 | integer,
7 | } from "drizzle-orm/pg-core";
8 |
9 | export const users = pgTable("users", {
10 | id: text("id").primaryKey(),
11 | name: text("name").notNull(),
12 | email: text("email").notNull().unique(),
13 | emailVerified: boolean("email_verified").notNull(),
14 | image: text("image"),
15 | createdAt: timestamp("created_at").notNull(),
16 | updatedAt: timestamp("updated_at").notNull(),
17 | stripeCustomerId: text("stripe_customer_id"),
18 | role: text("role"),
19 | banned: boolean("banned"),
20 | banReason: text("ban_reason"),
21 | banExpires: timestamp("ban_expires"),
22 | });
23 |
24 | export const sessions = pgTable("sessions", {
25 | id: text("id").primaryKey(),
26 | expiresAt: timestamp("expires_at").notNull(),
27 | token: text("token").notNull().unique(),
28 | createdAt: timestamp("created_at").notNull(),
29 | updatedAt: timestamp("updated_at").notNull(),
30 | ipAddress: text("ip_address"),
31 | userAgent: text("user_agent"),
32 | userId: text("user_id")
33 | .notNull()
34 | .references(() => users.id, { onDelete: "cascade" }),
35 | impersonatedBy: text("impersonated_by"),
36 | activeOrganizationId: text("active_organization_id"),
37 | });
38 |
39 | export const accounts = pgTable("accounts", {
40 | id: text("id").primaryKey(),
41 | accountId: text("account_id").notNull(),
42 | providerId: text("provider_id").notNull(),
43 | userId: text("user_id")
44 | .notNull()
45 | .references(() => users.id, { onDelete: "cascade" }),
46 | accessToken: text("access_token"),
47 | refreshToken: text("refresh_token"),
48 | idToken: text("id_token"),
49 | accessTokenExpiresAt: timestamp("access_token_expires_at"),
50 | refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
51 | scope: text("scope"),
52 | password: text("password"),
53 | createdAt: timestamp("created_at").notNull(),
54 | updatedAt: timestamp("updated_at").notNull(),
55 | });
56 |
57 | export const verifications = pgTable("verifications", {
58 | id: text("id").primaryKey(),
59 | identifier: text("identifier").notNull(),
60 | value: text("value").notNull(),
61 | expiresAt: timestamp("expires_at").notNull(),
62 | createdAt: timestamp("created_at"),
63 | updatedAt: timestamp("updated_at"),
64 | });
65 |
66 | export const subscriptions = pgTable("subscriptions", {
67 | id: text("id").primaryKey(),
68 | plan: text("plan").notNull(),
69 | referenceId: text("reference_id").notNull(),
70 | stripeCustomerId: text("stripe_customer_id"),
71 | stripeSubscriptionId: text("stripe_subscription_id"),
72 | status: text("status"),
73 | periodStart: timestamp("period_start"),
74 | periodEnd: timestamp("period_end"),
75 | cancelAtPeriodEnd: boolean("cancel_at_period_end"),
76 | seats: integer("seats"),
77 | });
78 |
79 | export const organizations = pgTable("organizations", {
80 | id: text("id").primaryKey(),
81 | name: text("name").notNull(),
82 | slug: text("slug").unique(),
83 | logo: text("logo"),
84 | createdAt: timestamp("created_at").notNull(),
85 | metadata: text("metadata"),
86 | });
87 |
88 | export const members = pgTable("members", {
89 | id: text("id").primaryKey(),
90 | organizationId: text("organization_id")
91 | .notNull()
92 | .references(() => organizations.id, { onDelete: "cascade" }),
93 | userId: text("user_id")
94 | .notNull()
95 | .references(() => users.id, { onDelete: "cascade" }),
96 | role: text("role").notNull(),
97 | createdAt: timestamp("created_at").notNull(),
98 | });
99 |
100 | export const invitations = pgTable("invitations", {
101 | id: text("id").primaryKey(),
102 | organizationId: text("organization_id")
103 | .notNull()
104 | .references(() => organizations.id, { onDelete: "cascade" }),
105 | email: text("email").notNull(),
106 | role: text("role"),
107 | status: text("status").notNull(),
108 | expiresAt: timestamp("expires_at").notNull(),
109 | inviterId: text("inviter_id")
110 | .notNull()
111 | .references(() => users.id, { onDelete: "cascade" }),
112 | });
113 |
--------------------------------------------------------------------------------
/packages/ui/src/components/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as DialogPrimitive from "@radix-ui/react-dialog";
5 | import { XIcon } from "lucide-react";
6 |
7 | import { cn } from "@neostack/ui/lib/utils";
8 |
9 | function Dialog({
10 | ...props
11 | }: React.ComponentProps) {
12 | return ;
13 | }
14 |
15 | function DialogTrigger({
16 | ...props
17 | }: React.ComponentProps) {
18 | return ;
19 | }
20 |
21 | function DialogPortal({
22 | ...props
23 | }: React.ComponentProps) {
24 | return ;
25 | }
26 |
27 | function DialogClose({
28 | ...props
29 | }: React.ComponentProps) {
30 | return ;
31 | }
32 |
33 | function DialogOverlay({
34 | className,
35 | ...props
36 | }: React.ComponentProps) {
37 | return (
38 |
46 | );
47 | }
48 |
49 | function DialogContent({
50 | className,
51 | children,
52 | ...props
53 | }: React.ComponentProps) {
54 | return (
55 |
56 |
57 |
65 | {children}
66 |
67 |
68 | Close
69 |
70 |
71 |
72 | );
73 | }
74 |
75 | function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
76 | return (
77 |
82 | );
83 | }
84 |
85 | function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
86 | return (
87 |
95 | );
96 | }
97 |
98 | function DialogTitle({
99 | className,
100 | ...props
101 | }: React.ComponentProps) {
102 | return (
103 |
108 | );
109 | }
110 |
111 | function DialogDescription({
112 | className,
113 | ...props
114 | }: React.ComponentProps) {
115 | return (
116 |
121 | );
122 | }
123 |
124 | export {
125 | Dialog,
126 | DialogClose,
127 | DialogContent,
128 | DialogDescription,
129 | DialogFooter,
130 | DialogHeader,
131 | DialogOverlay,
132 | DialogPortal,
133 | DialogTitle,
134 | DialogTrigger,
135 | };
136 |
--------------------------------------------------------------------------------
/packages/ui/src/components/form.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as LabelPrimitive from "@radix-ui/react-label";
5 | import { Slot } from "@radix-ui/react-slot";
6 | import {
7 | Controller,
8 | FormProvider,
9 | useFormContext,
10 | useFormState,
11 | type ControllerProps,
12 | type FieldPath,
13 | type FieldValues,
14 | } from "react-hook-form";
15 |
16 | import { cn } from "@neostack/ui/lib/utils";
17 | import { Label } from "@neostack/ui/components/label";
18 |
19 | const Form = FormProvider;
20 |
21 | type FormFieldContextValue<
22 | TFieldValues extends FieldValues = FieldValues,
23 | TName extends FieldPath = FieldPath,
24 | > = {
25 | name: TName;
26 | };
27 |
28 | const FormFieldContext = React.createContext(
29 | {} as FormFieldContextValue
30 | );
31 |
32 | const FormField = <
33 | TFieldValues extends FieldValues = FieldValues,
34 | TName extends FieldPath = FieldPath,
35 | >({
36 | ...props
37 | }: ControllerProps) => {
38 | return (
39 |
40 |
41 |
42 | );
43 | };
44 |
45 | const useFormField = () => {
46 | const fieldContext = React.useContext(FormFieldContext);
47 | const itemContext = React.useContext(FormItemContext);
48 | const { getFieldState } = useFormContext();
49 | const formState = useFormState({ name: fieldContext.name });
50 | const fieldState = getFieldState(fieldContext.name, formState);
51 |
52 | if (!fieldContext) {
53 | throw new Error("useFormField should be used within ");
54 | }
55 |
56 | const { id } = itemContext;
57 |
58 | return {
59 | id,
60 | name: fieldContext.name,
61 | formItemId: `${id}-form-item`,
62 | formDescriptionId: `${id}-form-item-description`,
63 | formMessageId: `${id}-form-item-message`,
64 | ...fieldState,
65 | };
66 | };
67 |
68 | type FormItemContextValue = {
69 | id: string;
70 | };
71 |
72 | const FormItemContext = React.createContext(
73 | {} as FormItemContextValue
74 | );
75 |
76 | function FormItem({ className, ...props }: React.ComponentProps<"div">) {
77 | const id = React.useId();
78 |
79 | return (
80 |
81 |
86 |
87 | );
88 | }
89 |
90 | function FormLabel({
91 | className,
92 | ...props
93 | }: React.ComponentProps) {
94 | const { error, formItemId } = useFormField();
95 |
96 | return (
97 |
104 | );
105 | }
106 |
107 | function FormControl({ ...props }: React.ComponentProps) {
108 | const { error, formItemId, formDescriptionId, formMessageId } =
109 | useFormField();
110 |
111 | return (
112 |
123 | );
124 | }
125 |
126 | function FormDescription({ className, ...props }: React.ComponentProps<"p">) {
127 | const { formDescriptionId } = useFormField();
128 |
129 | return (
130 |
136 | );
137 | }
138 |
139 | function FormMessage({ className, ...props }: React.ComponentProps<"p">) {
140 | const { error, formMessageId } = useFormField();
141 | const body = error ? String(error?.message ?? "") : props.children;
142 |
143 | if (!body) {
144 | return null;
145 | }
146 |
147 | return (
148 |
154 | {body}
155 |
156 | );
157 | }
158 |
159 | export {
160 | useFormField,
161 | Form,
162 | FormItem,
163 | FormLabel,
164 | FormControl,
165 | FormDescription,
166 | FormMessage,
167 | FormField,
168 | };
169 |
--------------------------------------------------------------------------------
/packages/ui/src/components/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog";
5 |
6 | import { cn } from "@neostack/ui/lib/utils";
7 | import { buttonVariants } from "@neostack/ui/components/button";
8 |
9 | function AlertDialog({
10 | ...props
11 | }: React.ComponentProps) {
12 | return ;
13 | }
14 |
15 | function AlertDialogTrigger({
16 | ...props
17 | }: React.ComponentProps) {
18 | return (
19 |
20 | );
21 | }
22 |
23 | function AlertDialogPortal({
24 | ...props
25 | }: React.ComponentProps) {
26 | return (
27 |
28 | );
29 | }
30 |
31 | function AlertDialogOverlay({
32 | className,
33 | ...props
34 | }: React.ComponentProps) {
35 | return (
36 |
44 | );
45 | }
46 |
47 | function AlertDialogContent({
48 | className,
49 | ...props
50 | }: React.ComponentProps) {
51 | return (
52 |
53 |
54 |
62 |
63 | );
64 | }
65 |
66 | function AlertDialogHeader({
67 | className,
68 | ...props
69 | }: React.ComponentProps<"div">) {
70 | return (
71 |
76 | );
77 | }
78 |
79 | function AlertDialogFooter({
80 | className,
81 | ...props
82 | }: React.ComponentProps<"div">) {
83 | return (
84 |
92 | );
93 | }
94 |
95 | function AlertDialogTitle({
96 | className,
97 | ...props
98 | }: React.ComponentProps) {
99 | return (
100 |
105 | );
106 | }
107 |
108 | function AlertDialogDescription({
109 | className,
110 | ...props
111 | }: React.ComponentProps) {
112 | return (
113 |
118 | );
119 | }
120 |
121 | function AlertDialogAction({
122 | className,
123 | ...props
124 | }: React.ComponentProps) {
125 | return (
126 |
130 | );
131 | }
132 |
133 | function AlertDialogCancel({
134 | className,
135 | ...props
136 | }: React.ComponentProps) {
137 | return (
138 |
142 | );
143 | }
144 |
145 | export {
146 | AlertDialog,
147 | AlertDialogPortal,
148 | AlertDialogOverlay,
149 | AlertDialogTrigger,
150 | AlertDialogContent,
151 | AlertDialogHeader,
152 | AlertDialogFooter,
153 | AlertDialogTitle,
154 | AlertDialogDescription,
155 | AlertDialogAction,
156 | AlertDialogCancel,
157 | };
158 |
--------------------------------------------------------------------------------
/packages/ui/src/components/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Drawer as DrawerPrimitive } from "vaul";
5 |
6 | import { cn } from "@neostack/ui/lib/utils";
7 |
8 | function Drawer({
9 | ...props
10 | }: React.ComponentProps) {
11 | return ;
12 | }
13 |
14 | function DrawerTrigger({
15 | ...props
16 | }: React.ComponentProps) {
17 | return ;
18 | }
19 |
20 | function DrawerPortal({
21 | ...props
22 | }: React.ComponentProps) {
23 | return ;
24 | }
25 |
26 | function DrawerClose({
27 | ...props
28 | }: React.ComponentProps) {
29 | return ;
30 | }
31 |
32 | function DrawerOverlay({
33 | className,
34 | ...props
35 | }: React.ComponentProps) {
36 | return (
37 |
45 | );
46 | }
47 |
48 | function DrawerContent({
49 | className,
50 | children,
51 | ...props
52 | }: React.ComponentProps) {
53 | return (
54 |
55 |
56 |
68 |
69 | {children}
70 |
71 |
72 | );
73 | }
74 |
75 | function DrawerHeader({ className, ...props }: React.ComponentProps<"div">) {
76 | return (
77 |
82 | );
83 | }
84 |
85 | function DrawerFooter({ className, ...props }: React.ComponentProps<"div">) {
86 | return (
87 |
92 | );
93 | }
94 |
95 | function DrawerTitle({
96 | className,
97 | ...props
98 | }: React.ComponentProps) {
99 | return (
100 |
105 | );
106 | }
107 |
108 | function DrawerDescription({
109 | className,
110 | ...props
111 | }: React.ComponentProps) {
112 | return (
113 |
118 | );
119 | }
120 |
121 | export {
122 | Drawer,
123 | DrawerPortal,
124 | DrawerOverlay,
125 | DrawerTrigger,
126 | DrawerClose,
127 | DrawerContent,
128 | DrawerHeader,
129 | DrawerFooter,
130 | DrawerTitle,
131 | DrawerDescription,
132 | };
133 |
--------------------------------------------------------------------------------
/packages/ui/src/components/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SheetPrimitive from "@radix-ui/react-dialog";
5 | import { XIcon } from "lucide-react";
6 |
7 | import { cn } from "@neostack/ui/lib/utils";
8 |
9 | function Sheet({ ...props }: React.ComponentProps) {
10 | return ;
11 | }
12 |
13 | function SheetTrigger({
14 | ...props
15 | }: React.ComponentProps) {
16 | return ;
17 | }
18 |
19 | function SheetClose({
20 | ...props
21 | }: React.ComponentProps) {
22 | return ;
23 | }
24 |
25 | function SheetPortal({
26 | ...props
27 | }: React.ComponentProps) {
28 | return ;
29 | }
30 |
31 | function SheetOverlay({
32 | className,
33 | ...props
34 | }: React.ComponentProps) {
35 | return (
36 |
44 | );
45 | }
46 |
47 | function SheetContent({
48 | className,
49 | children,
50 | side = "right",
51 | ...props
52 | }: React.ComponentProps & {
53 | side?: "top" | "right" | "bottom" | "left";
54 | }) {
55 | return (
56 |
57 |
58 |
74 | {children}
75 |
76 |
77 | Close
78 |
79 |
80 |
81 | );
82 | }
83 |
84 | function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
85 | return (
86 |
91 | );
92 | }
93 |
94 | function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
95 | return (
96 |
101 | );
102 | }
103 |
104 | function SheetTitle({
105 | className,
106 | ...props
107 | }: React.ComponentProps) {
108 | return (
109 |
114 | );
115 | }
116 |
117 | function SheetDescription({
118 | className,
119 | ...props
120 | }: React.ComponentProps) {
121 | return (
122 |
127 | );
128 | }
129 |
130 | export {
131 | Sheet,
132 | SheetTrigger,
133 | SheetClose,
134 | SheetContent,
135 | SheetHeader,
136 | SheetFooter,
137 | SheetTitle,
138 | SheetDescription,
139 | };
140 |
--------------------------------------------------------------------------------
/packages/database/migrations/0000_daffy_ego.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE "accounts" (
2 | "id" text PRIMARY KEY NOT NULL,
3 | "account_id" text NOT NULL,
4 | "provider_id" text NOT NULL,
5 | "user_id" text NOT NULL,
6 | "access_token" text,
7 | "refresh_token" text,
8 | "id_token" text,
9 | "access_token_expires_at" timestamp,
10 | "refresh_token_expires_at" timestamp,
11 | "scope" text,
12 | "password" text,
13 | "created_at" timestamp NOT NULL,
14 | "updated_at" timestamp NOT NULL
15 | );
16 | --> statement-breakpoint
17 | CREATE TABLE "invitations" (
18 | "id" text PRIMARY KEY NOT NULL,
19 | "organization_id" text NOT NULL,
20 | "email" text NOT NULL,
21 | "role" text,
22 | "status" text NOT NULL,
23 | "expires_at" timestamp NOT NULL,
24 | "inviter_id" text NOT NULL
25 | );
26 | --> statement-breakpoint
27 | CREATE TABLE "members" (
28 | "id" text PRIMARY KEY NOT NULL,
29 | "organization_id" text NOT NULL,
30 | "user_id" text NOT NULL,
31 | "role" text NOT NULL,
32 | "created_at" timestamp NOT NULL
33 | );
34 | --> statement-breakpoint
35 | CREATE TABLE "organizations" (
36 | "id" text PRIMARY KEY NOT NULL,
37 | "name" text NOT NULL,
38 | "slug" text,
39 | "logo" text,
40 | "created_at" timestamp NOT NULL,
41 | "metadata" text,
42 | CONSTRAINT "organizations_slug_unique" UNIQUE("slug")
43 | );
44 | --> statement-breakpoint
45 | CREATE TABLE "sessions" (
46 | "id" text PRIMARY KEY NOT NULL,
47 | "expires_at" timestamp NOT NULL,
48 | "token" text NOT NULL,
49 | "created_at" timestamp NOT NULL,
50 | "updated_at" timestamp NOT NULL,
51 | "ip_address" text,
52 | "user_agent" text,
53 | "user_id" text NOT NULL,
54 | "impersonated_by" text,
55 | "active_organization_id" text,
56 | CONSTRAINT "sessions_token_unique" UNIQUE("token")
57 | );
58 | --> statement-breakpoint
59 | CREATE TABLE "subscriptions" (
60 | "id" text PRIMARY KEY NOT NULL,
61 | "plan" text NOT NULL,
62 | "reference_id" text NOT NULL,
63 | "stripe_customer_id" text,
64 | "stripe_subscription_id" text,
65 | "status" text,
66 | "period_start" timestamp,
67 | "period_end" timestamp,
68 | "cancel_at_period_end" boolean,
69 | "seats" integer
70 | );
71 | --> statement-breakpoint
72 | CREATE TABLE "users" (
73 | "id" text PRIMARY KEY NOT NULL,
74 | "name" text NOT NULL,
75 | "email" text NOT NULL,
76 | "email_verified" boolean NOT NULL,
77 | "image" text,
78 | "created_at" timestamp NOT NULL,
79 | "updated_at" timestamp NOT NULL,
80 | "stripe_customer_id" text,
81 | "role" text,
82 | "banned" boolean,
83 | "ban_reason" text,
84 | "ban_expires" timestamp,
85 | CONSTRAINT "users_email_unique" UNIQUE("email")
86 | );
87 | --> statement-breakpoint
88 | CREATE TABLE "verifications" (
89 | "id" text PRIMARY KEY NOT NULL,
90 | "identifier" text NOT NULL,
91 | "value" text NOT NULL,
92 | "expires_at" timestamp NOT NULL,
93 | "created_at" timestamp,
94 | "updated_at" timestamp
95 | );
96 | --> statement-breakpoint
97 | CREATE TABLE "transcript_chunks" (
98 | "id" text PRIMARY KEY NOT NULL,
99 | "transcription_id" text NOT NULL,
100 | "chunk_index" integer NOT NULL,
101 | "chunk_text" text NOT NULL,
102 | "embedding" vector(1024),
103 | "created_at" timestamp DEFAULT now() NOT NULL
104 | );
105 | --> statement-breakpoint
106 | CREATE TABLE "transcriptions" (
107 | "id" text PRIMARY KEY NOT NULL,
108 | "title" text NOT NULL,
109 | "summary" text NOT NULL,
110 | "audio_path" text NOT NULL,
111 | "srt_path" text,
112 | "transcript_path" text,
113 | "user_id" text NOT NULL,
114 | "created_at" timestamp DEFAULT now() NOT NULL,
115 | "updated_at" timestamp DEFAULT now() NOT NULL
116 | );
117 | --> statement-breakpoint
118 | ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
119 | ALTER TABLE "invitations" ADD CONSTRAINT "invitations_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
120 | ALTER TABLE "invitations" ADD CONSTRAINT "invitations_inviter_id_users_id_fk" FOREIGN KEY ("inviter_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
121 | ALTER TABLE "members" ADD CONSTRAINT "members_organization_id_organizations_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organizations"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
122 | ALTER TABLE "members" ADD CONSTRAINT "members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
123 | ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
124 | ALTER TABLE "transcript_chunks" ADD CONSTRAINT "transcript_chunks_transcription_id_transcriptions_id_fk" FOREIGN KEY ("transcription_id") REFERENCES "public"."transcriptions"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
125 | ALTER TABLE "transcriptions" ADD CONSTRAINT "transcriptions_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;
--------------------------------------------------------------------------------
/apps/api/src/durables/NotificationWebsocketServer.ts:
--------------------------------------------------------------------------------
1 | import { Notification, notificationSchema } from "@/client";
2 | import { AppEnv } from "@/types/AppEnv";
3 | import { zValidator } from "@hono/zod-validator";
4 | import { DurableObject } from "cloudflare:workers";
5 | import { Hono } from "hono";
6 |
7 | export class NotificationWebsocketServer extends DurableObject {
8 | private app: Hono = new Hono();
9 | private notifications: Map = new Map();
10 |
11 | constructor(state: DurableObjectState, env: AppEnv) {
12 | super(state, env);
13 |
14 | // Initialize routes
15 | this.app.post("/create", async (c) => {
16 | try {
17 | const message = await c.req.json();
18 |
19 | await this.ctx.storage.put(`message:${message.id}`, message);
20 | this.notifications.set(message.id, message);
21 | for (const ws of this.ctx.getWebSockets()) {
22 | ws.send(JSON.stringify(message));
23 | }
24 | return c.json({ success: true }, 200);
25 | } catch (error) {
26 | console.error("Error creating notification:", error);
27 | return c.json({ error: "Failed to create notification" }, 500);
28 | }
29 | });
30 |
31 | this.app.post("/clear", async (c) => {
32 | try {
33 | // Clear all notifications from storage and in-memory map
34 | await this.ctx.storage.deleteAll();
35 | this.notifications.clear();
36 | for (const ws of this.ctx.getWebSockets()) {
37 | ws.send(
38 | JSON.stringify({
39 | type: "clear",
40 | })
41 | );
42 | }
43 | return c.json({ success: true }, 200);
44 | } catch (error) {
45 | console.error("Error clearing notifications:", error);
46 | return c.json({ error: "Failed to clear notifications" }, 500);
47 | }
48 | });
49 |
50 | this.app.post("/clear/:id", async (c) => {
51 | const { id } = c.req.param();
52 | try {
53 | // Clear all notifications from storage and in-memory map
54 | await this.ctx.storage.delete(id);
55 | this.notifications.delete(id);
56 | for (const ws of this.ctx.getWebSockets()) {
57 | ws.send(
58 | JSON.stringify({
59 | type: "clear-id",
60 | payload: id,
61 | })
62 | );
63 | }
64 | return c.json({ success: true }, 200);
65 | } catch (error) {
66 | console.error("Error clearing notifications:", error);
67 | return c.json({ error: "Failed to clear notifications" }, 500);
68 | }
69 | });
70 |
71 | // Load existing notifications
72 | this.ctx.blockConcurrencyWhile(async () => {
73 | try {
74 | const storedNotifications = await this.ctx.storage.list({
75 | prefix: "message:",
76 | });
77 |
78 | for (const [key, value] of storedNotifications) {
79 | const notificationId = key.replace("message:", "");
80 | this.notifications.set(notificationId, value);
81 | }
82 | } catch (error) {
83 | console.error("Error loading stored notifications:", error);
84 | }
85 | });
86 | }
87 |
88 | async webSocketMessage(ws: WebSocket, message: string | ArrayBuffer) {
89 | try {
90 | const msg =
91 | typeof message === "string"
92 | ? message
93 | : new TextDecoder().decode(message);
94 |
95 | if (msg.toLowerCase() === "ping") {
96 | ws.send("pong");
97 | return;
98 | }
99 |
100 | const data = JSON.parse(msg);
101 | ws.send(JSON.stringify({ type: "received", data }));
102 | } catch (error) {
103 | console.error("WebSocket message error:", error);
104 | ws.send(JSON.stringify({ error: "Invalid message" }));
105 | }
106 | }
107 |
108 | async webSocketClose(
109 | ws: WebSocket,
110 | code: number,
111 | reason: string,
112 | wasClean: boolean
113 | ) {
114 | try {
115 | ws.close(code, "Durable Object is closing WebSocket");
116 | } catch (error) {
117 | console.error("Error closing WebSocket:", error);
118 | }
119 | }
120 |
121 | async webSocketError(ws: WebSocket, error: Error) {
122 | console.error("WebSocket error:", error);
123 | try {
124 | ws.close(1001, "WebSocket error occurred");
125 | } catch (error) {
126 | console.error("Error closing WebSocket:", error);
127 | }
128 | }
129 |
130 | async fetch(request: Request) {
131 | try {
132 | if (request.headers.get("Upgrade") === "websocket") {
133 | const pair = new WebSocketPair();
134 | const [client, server] = Object.values(pair);
135 |
136 | this.ctx.acceptWebSocket(server);
137 |
138 | const initialMessage = {
139 | type: "initialState",
140 | payload: Array.from(this.notifications.values()).map(
141 | (notification) => notification
142 | ),
143 | };
144 | server.send(JSON.stringify(initialMessage));
145 |
146 | return new Response(null, {
147 | status: 101,
148 | webSocket: client,
149 | });
150 | }
151 | return await this.app.fetch(request);
152 | } catch (error) {
153 | console.error("Fetch error:", error);
154 | return new Response("Internal Server Error", { status: 500 });
155 | }
156 | }
157 | }
158 |
--------------------------------------------------------------------------------
/packages/ui/src/components/command.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import { Command as CommandPrimitive } from "cmdk";
5 | import { SearchIcon } from "lucide-react";
6 |
7 | import { cn } from "@neostack/ui/lib/utils";
8 | import {
9 | Dialog,
10 | DialogContent,
11 | DialogDescription,
12 | DialogHeader,
13 | DialogTitle,
14 | } from "@neostack/ui/components/dialog";
15 |
16 | function Command({
17 | className,
18 | ...props
19 | }: React.ComponentProps) {
20 | return (
21 |
29 | );
30 | }
31 |
32 | function CommandDialog({
33 | title = "Command Palette",
34 | description = "Search for a command to run...",
35 | children,
36 | ...props
37 | }: React.ComponentProps & {
38 | title?: string;
39 | description?: string;
40 | }) {
41 | return (
42 |
53 | );
54 | }
55 |
56 | function CommandInput({
57 | className,
58 | ...props
59 | }: React.ComponentProps) {
60 | return (
61 |
65 |
66 |
74 |
75 | );
76 | }
77 |
78 | function CommandList({
79 | className,
80 | ...props
81 | }: React.ComponentProps) {
82 | return (
83 |
91 | );
92 | }
93 |
94 | function CommandEmpty({
95 | ...props
96 | }: React.ComponentProps) {
97 | return (
98 |
103 | );
104 | }
105 |
106 | function CommandGroup({
107 | className,
108 | ...props
109 | }: React.ComponentProps) {
110 | return (
111 |
119 | );
120 | }
121 |
122 | function CommandSeparator({
123 | className,
124 | ...props
125 | }: React.ComponentProps) {
126 | return (
127 |
132 | );
133 | }
134 |
135 | function CommandItem({
136 | className,
137 | ...props
138 | }: React.ComponentProps) {
139 | return (
140 |
148 | );
149 | }
150 |
151 | function CommandShortcut({
152 | className,
153 | ...props
154 | }: React.ComponentProps<"span">) {
155 | return (
156 |
164 | );
165 | }
166 |
167 | export {
168 | Command,
169 | CommandDialog,
170 | CommandInput,
171 | CommandList,
172 | CommandEmpty,
173 | CommandGroup,
174 | CommandItem,
175 | CommandShortcut,
176 | CommandSeparator,
177 | };
178 |
--------------------------------------------------------------------------------
/apps/web/src/components/PagesResetPassword.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { zodResolver } from "@hookform/resolvers/zod";
3 | import { useForm } from "react-hook-form";
4 | import { z } from "zod";
5 | import { Button } from "@neostack/ui/components/button";
6 | import {
7 | Card,
8 | CardHeader,
9 | CardTitle,
10 | CardDescription,
11 | CardContent,
12 | CardFooter,
13 | } from "@neostack/ui/components/card";
14 | import {
15 | Form,
16 | FormControl,
17 | FormField,
18 | FormItem,
19 | FormLabel,
20 | FormMessage,
21 | } from "@neostack/ui/components/form";
22 | import { Input } from "@neostack/ui/components/input";
23 | import { authClient } from "@/lib/authClient";
24 | import { toast } from "sonner";
25 | import { navigate } from "astro/virtual-modules/transitions-router.js";
26 |
27 | // Form schema
28 | const resetPasswordSchema = z
29 | .object({
30 | password: z.string().min(8, "Password must be at least 8 characters"),
31 | confirmPassword: z.string().min(8, "Please confirm your password"),
32 | })
33 | .refine((data) => data.password === data.confirmPassword, {
34 | message: "Passwords do not match",
35 | path: ["confirmPassword"],
36 | });
37 |
38 | type ResetPasswordForm = z.infer;
39 |
40 | export function PagesResetPassword() {
41 | const [isSubmitting, setIsSubmitting] = useState(false);
42 |
43 | // Form setup
44 | const resetForm = useForm({
45 | resolver: zodResolver(resetPasswordSchema),
46 | defaultValues: {
47 | password: "",
48 | confirmPassword: "",
49 | },
50 | });
51 |
52 | // Form submission handler
53 | const onSubmit = async (values: ResetPasswordForm) => {
54 | setIsSubmitting(true);
55 | const token = new URLSearchParams(window.location.search).get("token");
56 | if (!token) {
57 | toast.error("Invalid token. Please try again.");
58 | setIsSubmitting(false);
59 | return;
60 | }
61 | await authClient.resetPassword({
62 | newPassword: values.password,
63 | token,
64 | fetchOptions: {
65 | onSuccess: async () => {
66 | toast.success("Password reset successfully. You can now sign in.");
67 | resetForm.reset();
68 | setIsSubmitting(false);
69 | window.location.href = "/login";
70 | },
71 | onError: (res) => {
72 | const errorMessage = res.error.message || "An error occurred";
73 | toast.error(errorMessage);
74 | resetForm.setError("root", {
75 | message: errorMessage,
76 | });
77 | setIsSubmitting(false);
78 | },
79 | },
80 | });
81 | };
82 |
83 | return (
84 |
85 |
86 |
87 | Reset your password
88 |
89 | Enter a new password for your account
90 |
91 |
92 |
93 |
143 |
144 |
145 |
146 |
155 |
156 |
157 |
158 | );
159 | }
160 |
--------------------------------------------------------------------------------
/apps/web/src/components/PagesHome.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@neostack/ui/components/button";
2 | import {
3 | Card,
4 | CardContent,
5 | CardHeader,
6 | CardTitle,
7 | } from "@neostack/ui/components/card";
8 | import { ArrowRight } from "lucide-react";
9 | export function PagesHome() {
10 | return (
11 |
12 | {/* Hero Section */}
13 |
14 |
15 |
16 |
17 | Build the Future
18 |
19 |
20 | Unleash your creativity with our cutting-edge platform. Fast,
21 | intuitive, and built for innovators.
22 |
23 |
42 |
43 |
44 |
45 | {/* Features Section */}
46 |
47 |
48 |
49 | Why We're Different
50 |
51 |
52 | {/* Feature 1 */}
53 |
54 |
55 |
56 | Lightning Fast
57 |
58 |
59 |
60 |
61 | Optimized for speed, our platform delivers instant results
62 | without compromise.
63 |
64 |
65 |
66 | {/* Feature 2 */}
67 |
68 |
69 |
70 | Seamless Design
71 |
72 |
73 |
74 |
75 | Intuitive interfaces that make complex tasks feel effortless
76 | and natural.
77 |
78 |
79 |
80 | {/* Feature 3 */}
81 |
82 |
83 |
84 | Always On Support
85 |
86 |
87 |
88 |
89 | Our team is here 24/7 to ensure you succeed at every step.
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 | {/* CTA Section */}
98 |
99 |
100 |
101 | Ready to Elevate Your Game?
102 |
103 |
104 | Join thousands of creators and start building something
105 | extraordinary today.
106 |
107 |
114 |
115 |
116 |
117 | );
118 | }
119 |
--------------------------------------------------------------------------------
/apps/web/src/components/PagesVerifyEmail.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { zodResolver } from "@hookform/resolvers/zod";
3 | import { useForm } from "react-hook-form";
4 | import { z } from "zod";
5 | import { Button } from "@neostack/ui/components/button";
6 | import {
7 | Card,
8 | CardHeader,
9 | CardTitle,
10 | CardDescription,
11 | CardContent,
12 | CardFooter,
13 | } from "@neostack/ui/components/card";
14 | import {
15 | Form,
16 | FormControl,
17 | FormField,
18 | FormItem,
19 | FormLabel,
20 | FormMessage,
21 | } from "@neostack/ui/components/form";
22 | import { Input } from "@neostack/ui/components/input";
23 | import { authClient } from "@/lib/authClient";
24 | import { useQueryState } from "nuqs";
25 | import { NuqsAdapter } from "nuqs/adapters/react";
26 | import { toast } from "sonner";
27 |
28 | // Form schema
29 | const verifyEmailSchema = z.object({
30 | email: z.string().email("Invalid email address"),
31 | });
32 |
33 | type VerifyEmailForm = z.infer;
34 |
35 | export function PagesVerifyEmail() {
36 | return (
37 |
38 |
39 |
40 | );
41 | }
42 |
43 | function Page() {
44 | const [isSending, setIsSending] = useState(false);
45 |
46 | // Get email from URL query using nuqs
47 | const [emailQuery] = useQueryState("email");
48 |
49 | // Initialize form with email from query if available
50 | const verifyForm = useForm({
51 | resolver: zodResolver(verifyEmailSchema),
52 | defaultValues: {
53 | email: emailQuery || "",
54 | },
55 | });
56 |
57 | // Update form email and send link if query changes
58 | useEffect(() => {
59 | if (emailQuery) {
60 | verifyForm.setValue("email", emailQuery);
61 | handleSendLink(emailQuery);
62 | }
63 | }, [emailQuery]);
64 |
65 | // Handler for sending/resending verification link
66 | const handleSendLink = (email: string) => {
67 | if (!email) {
68 | verifyForm.setError("root", {
69 | message: "Please enter an email address.",
70 | });
71 | return;
72 | }
73 |
74 | setIsSending(true);
75 | authClient.sendVerificationEmail({
76 | email,
77 | callbackURL: `${import.meta.env.PUBLIC_SITE_URL}`,
78 | fetchOptions: {
79 | onSuccess: () => {
80 | toast.success("A verification link has been sent to your email.");
81 | verifyForm.clearErrors("root");
82 | setIsSending(false);
83 | },
84 | onError: (error) => {
85 | console.error("Send verification email error:", error);
86 | verifyForm.setError("root", {
87 | message: "Failed to send verification link. Please try again.",
88 | });
89 | setIsSending(false);
90 | },
91 | },
92 | });
93 | };
94 |
95 | // Form submission handler
96 | const onSubmit = (values: VerifyEmailForm) => {
97 | handleSendLink(values.email);
98 | };
99 |
100 | return (
101 |
102 |
103 |
104 | Verify your email
105 |
106 | Enter your email address to receive a verification link
107 |
108 |
109 |
110 |
143 |
144 |
145 |
146 |
147 | Didn't receive a link?{" "}
148 |
157 |
158 |
167 |
168 |
169 |
170 | );
171 | }
172 |
--------------------------------------------------------------------------------
/apps/web/src/components/PagesDashboardTranscripts.tsx:
--------------------------------------------------------------------------------
1 | import { useState, useEffect } from "react";
2 | import { Button } from "@neostack/ui/components/button";
3 | import {
4 | Card,
5 | CardHeader,
6 | CardTitle,
7 | CardDescription,
8 | CardContent,
9 | } from "@neostack/ui/components/card";
10 | import {
11 | Table,
12 | TableBody,
13 | TableCell,
14 | TableHead,
15 | TableHeader,
16 | TableRow,
17 | } from "@neostack/ui/components/table";
18 | import { toast } from "sonner";
19 | import { apiClient } from "@/lib/apiClient";
20 | import { Loader2 } from "lucide-react";
21 | import type { InferResponseType } from "hono/client";
22 |
23 | interface Pagination {
24 | page: number;
25 | limit: number;
26 | totalCount: number;
27 | totalPages: number;
28 | hasNextPage: boolean;
29 | hasPreviousPage: boolean;
30 | }
31 |
32 | export function PagesDashboardTranscripts() {
33 | const [transcripts, setTranscripts] = useState<
34 | InferResponseType["data"]
35 | >([]);
36 | const [pagination, setPagination] = useState({
37 | page: 1,
38 | limit: 10,
39 | totalCount: 0,
40 | totalPages: 1,
41 | hasNextPage: false,
42 | hasPreviousPage: false,
43 | });
44 | const [loading, setLoading] = useState(false);
45 | const [error, setError] = useState(null);
46 |
47 | // Fetch transcripts from the API
48 | const fetchTranscripts = async (page: number, limit: number) => {
49 | setLoading(true);
50 | setError(null);
51 | try {
52 | const response = await apiClient.v1.transcripts.$get({
53 | query: { page: page.toString(), limit: limit.toString() },
54 | });
55 | if (!response.ok) {
56 | const data = await response.json();
57 | throw new Error(data.message || "Failed to fetch transcripts");
58 | }
59 | const data = await response.json();
60 | setTranscripts(data.data);
61 | setPagination(data.pagination);
62 | } catch (err) {
63 | const errorMessage =
64 | err instanceof Error ? err.message : "An unexpected error occurred";
65 | setError(errorMessage);
66 | toast.error(errorMessage);
67 | } finally {
68 | setLoading(false);
69 | }
70 | };
71 |
72 | // Initial fetch and refetch on page/limit change
73 | useEffect(() => {
74 | fetchTranscripts(pagination.page, pagination.limit);
75 | }, []);
76 |
77 | // Handle page change
78 | const handlePageChange = (newPage: number) => {
79 | if (newPage >= 1 && newPage <= pagination.totalPages) {
80 | fetchTranscripts(newPage, pagination.limit);
81 | }
82 | };
83 |
84 | // Format date for display
85 | const formatDate = (dateString: string) => {
86 | return new Date(dateString).toLocaleDateString("en-US", {
87 | year: "numeric",
88 | month: "short",
89 | day: "numeric",
90 | hour: "2-digit",
91 | minute: "2-digit",
92 | });
93 | };
94 |
95 | return (
96 |
97 |
98 | Transcripts Dashboard
99 |
100 |
101 |
102 |
103 | Your Transcripts
104 |
105 | View and manage your transcription jobs.
106 |
107 |
108 |
109 | {loading ? (
110 |
111 |
112 | Loading transcripts...
113 |
114 | ) : error ? (
115 | {error}
116 | ) : transcripts.length === 0 ? (
117 |
118 | No transcripts found. Upload an audio file to get started.
119 |
120 | ) : (
121 | <>
122 |
123 |
124 |
125 | Title
126 | Created At
127 |
128 |
129 |
130 | {transcripts.map((transcript) => (
131 |
132 |
133 |
138 |
139 |
140 | {formatDate(transcript.createdAt)}
141 |
142 |
143 | ))}
144 |
145 |
146 | {/* Pagination Controls */}
147 |
148 |
149 | Showing {transcripts.length} of {pagination.totalCount}{" "}
150 | transcripts
151 |
152 |
153 |
161 |
162 | Page {pagination.page} of {pagination.totalPages}
163 |
164 |
172 |
173 |
174 | >
175 | )}
176 |
177 |
178 |
179 |
180 | );
181 | }
182 |
--------------------------------------------------------------------------------
/packages/ui/src/components/carousel.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import useEmblaCarousel, {
5 | type UseEmblaCarouselType,
6 | } from "embla-carousel-react";
7 | import { ArrowLeft, ArrowRight } from "lucide-react";
8 |
9 | import { cn } from "@neostack/ui/lib/utils";
10 | import { Button } from "@neostack/ui/components/button";
11 |
12 | type CarouselApi = UseEmblaCarouselType[1];
13 | type UseCarouselParameters = Parameters;
14 | type CarouselOptions = UseCarouselParameters[0];
15 | type CarouselPlugin = UseCarouselParameters[1];
16 |
17 | type CarouselProps = {
18 | opts?: CarouselOptions;
19 | plugins?: CarouselPlugin;
20 | orientation?: "horizontal" | "vertical";
21 | setApi?: (api: CarouselApi) => void;
22 | };
23 |
24 | type CarouselContextProps = {
25 | carouselRef: ReturnType[0];
26 | api: ReturnType[1];
27 | scrollPrev: () => void;
28 | scrollNext: () => void;
29 | canScrollPrev: boolean;
30 | canScrollNext: boolean;
31 | } & CarouselProps;
32 |
33 | const CarouselContext = React.createContext(null);
34 |
35 | function useCarousel() {
36 | const context = React.useContext(CarouselContext);
37 |
38 | if (!context) {
39 | throw new Error("useCarousel must be used within a ");
40 | }
41 |
42 | return context;
43 | }
44 |
45 | function Carousel({
46 | orientation = "horizontal",
47 | opts,
48 | setApi,
49 | plugins,
50 | className,
51 | children,
52 | ...props
53 | }: React.ComponentProps<"div"> & CarouselProps) {
54 | const [carouselRef, api] = useEmblaCarousel(
55 | {
56 | ...opts,
57 | axis: orientation === "horizontal" ? "x" : "y",
58 | },
59 | plugins
60 | );
61 | const [canScrollPrev, setCanScrollPrev] = React.useState(false);
62 | const [canScrollNext, setCanScrollNext] = React.useState(false);
63 |
64 | const onSelect = React.useCallback((api: CarouselApi) => {
65 | if (!api) return;
66 | setCanScrollPrev(api.canScrollPrev());
67 | setCanScrollNext(api.canScrollNext());
68 | }, []);
69 |
70 | const scrollPrev = React.useCallback(() => {
71 | api?.scrollPrev();
72 | }, [api]);
73 |
74 | const scrollNext = React.useCallback(() => {
75 | api?.scrollNext();
76 | }, [api]);
77 |
78 | const handleKeyDown = React.useCallback(
79 | (event: React.KeyboardEvent) => {
80 | if (event.key === "ArrowLeft") {
81 | event.preventDefault();
82 | scrollPrev();
83 | } else if (event.key === "ArrowRight") {
84 | event.preventDefault();
85 | scrollNext();
86 | }
87 | },
88 | [scrollPrev, scrollNext]
89 | );
90 |
91 | React.useEffect(() => {
92 | if (!api || !setApi) return;
93 | setApi(api);
94 | }, [api, setApi]);
95 |
96 | React.useEffect(() => {
97 | if (!api) return;
98 | onSelect(api);
99 | api.on("reInit", onSelect);
100 | api.on("select", onSelect);
101 |
102 | return () => {
103 | api?.off("select", onSelect);
104 | };
105 | }, [api, onSelect]);
106 |
107 | return (
108 |
121 |
129 | {children}
130 |
131 |
132 | );
133 | }
134 |
135 | function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
136 | const { carouselRef, orientation } = useCarousel();
137 |
138 | return (
139 |
153 | );
154 | }
155 |
156 | function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
157 | const { orientation } = useCarousel();
158 |
159 | return (
160 |
171 | );
172 | }
173 |
174 | function CarouselPrevious({
175 | className,
176 | variant = "outline",
177 | size = "icon",
178 | ...props
179 | }: React.ComponentProps) {
180 | const { orientation, scrollPrev, canScrollPrev } = useCarousel();
181 |
182 | return (
183 |
201 | );
202 | }
203 |
204 | function CarouselNext({
205 | className,
206 | variant = "outline",
207 | size = "icon",
208 | ...props
209 | }: React.ComponentProps) {
210 | const { orientation, scrollNext, canScrollNext } = useCarousel();
211 |
212 | return (
213 |
231 | );
232 | }
233 |
234 | export {
235 | type CarouselApi,
236 | Carousel,
237 | CarouselContent,
238 | CarouselItem,
239 | CarouselPrevious,
240 | CarouselNext,
241 | };
242 |
--------------------------------------------------------------------------------
/packages/ui/src/components/select.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import * as React from "react";
4 | import * as SelectPrimitive from "@radix-ui/react-select";
5 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
6 |
7 | import { cn } from "@neostack/ui/lib/utils";
8 |
9 | function Select({
10 | ...props
11 | }: React.ComponentProps) {
12 | return ;
13 | }
14 |
15 | function SelectGroup({
16 | ...props
17 | }: React.ComponentProps) {
18 | return ;
19 | }
20 |
21 | function SelectValue({
22 | ...props
23 | }: React.ComponentProps) {
24 | return ;
25 | }
26 |
27 | function SelectTrigger({
28 | className,
29 | size = "default",
30 | children,
31 | ...props
32 | }: React.ComponentProps & {
33 | size?: "sm" | "default";
34 | }) {
35 | return (
36 |
45 | {children}
46 |
47 |
48 |
49 |
50 | );
51 | }
52 |
53 | function SelectContent({
54 | className,
55 | children,
56 | position = "popper",
57 | ...props
58 | }: React.ComponentProps) {
59 | return (
60 |
61 |
72 |
73 |
80 | {children}
81 |
82 |
83 |
84 |
85 | );
86 | }
87 |
88 | function SelectLabel({
89 | className,
90 | ...props
91 | }: React.ComponentProps) {
92 | return (
93 |
98 | );
99 | }
100 |
101 | function SelectItem({
102 | className,
103 | children,
104 | ...props
105 | }: React.ComponentProps) {
106 | return (
107 |
115 |
116 |
117 |
118 |
119 |
120 | {children}
121 |
122 | );
123 | }
124 |
125 | function SelectSeparator({
126 | className,
127 | ...props
128 | }: React.ComponentProps) {
129 | return (
130 |
135 | );
136 | }
137 |
138 | function SelectScrollUpButton({
139 | className,
140 | ...props
141 | }: React.ComponentProps) {
142 | return (
143 |
151 |
152 |
153 | );
154 | }
155 |
156 | function SelectScrollDownButton({
157 | className,
158 | ...props
159 | }: React.ComponentProps) {
160 | return (
161 |
169 |
170 |
171 | );
172 | }
173 |
174 | export {
175 | Select,
176 | SelectContent,
177 | SelectGroup,
178 | SelectItem,
179 | SelectLabel,
180 | SelectScrollDownButton,
181 | SelectScrollUpButton,
182 | SelectSeparator,
183 | SelectTrigger,
184 | SelectValue,
185 | };
186 |
--------------------------------------------------------------------------------
/packages/ui/src/components/navigation-menu.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu";
3 | import { cva } from "class-variance-authority";
4 | import { ChevronDownIcon } from "lucide-react";
5 |
6 | import { cn } from "@neostack/ui/lib/utils";
7 |
8 | function NavigationMenu({
9 | className,
10 | children,
11 | viewport = true,
12 | ...props
13 | }: React.ComponentProps & {
14 | viewport?: boolean;
15 | }) {
16 | return (
17 |
26 | {children}
27 | {viewport && }
28 |
29 | );
30 | }
31 |
32 | function NavigationMenuList({
33 | className,
34 | ...props
35 | }: React.ComponentProps) {
36 | return (
37 |
45 | );
46 | }
47 |
48 | function NavigationMenuItem({
49 | className,
50 | ...props
51 | }: React.ComponentProps) {
52 | return (
53 |
58 | );
59 | }
60 |
61 | const navigationMenuTriggerStyle = cva(
62 | "group inline-flex justify-center items-center bg-background data-[state=open]:bg-accent/50 data-[state=open]:hover:bg-accent data-[state=open]:focus:bg-accent hover:bg-accent focus:bg-accent disabled:opacity-50 px-4 py-2 rounded-md outline-none focus-visible:outline-1 focus-visible:ring-[3px] focus-visible:ring-ring/50 w-max h-9 font-medium text-sm transition-[color,box-shadow] data-[state=open]:text-accent-foreground hover:text-accent-foreground focus:text-accent-foreground disabled:pointer-events-none"
63 | );
64 |
65 | function NavigationMenuTrigger({
66 | className,
67 | children,
68 | ...props
69 | }: React.ComponentProps) {
70 | return (
71 |
76 | {children}{" "}
77 |
81 |
82 | );
83 | }
84 |
85 | function NavigationMenuContent({
86 | className,
87 | ...props
88 | }: React.ComponentProps) {
89 | return (
90 |
99 | );
100 | }
101 |
102 | function NavigationMenuViewport({
103 | className,
104 | ...props
105 | }: React.ComponentProps) {
106 | return (
107 |
112 |
120 |
121 | );
122 | }
123 |
124 | function NavigationMenuLink({
125 | className,
126 | ...props
127 | }: React.ComponentProps) {
128 | return (
129 |
137 | );
138 | }
139 |
140 | function NavigationMenuIndicator({
141 | className,
142 | ...props
143 | }: React.ComponentProps) {
144 | return (
145 |
153 |
154 |
155 | );
156 | }
157 |
158 | export {
159 | NavigationMenu,
160 | NavigationMenuList,
161 | NavigationMenuItem,
162 | NavigationMenuContent,
163 | NavigationMenuTrigger,
164 | NavigationMenuLink,
165 | NavigationMenuIndicator,
166 | NavigationMenuViewport,
167 | navigationMenuTriggerStyle,
168 | };
169 |
--------------------------------------------------------------------------------
|