Skip to content

Commit 173771a

Browse files
committed
reorder modules
1 parent a31befd commit 173771a

32 files changed

+1494
-288
lines changed

.cursor/rules/my-code-rules.mdc

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,16 @@ When a page is loading, please remember to add a loader to the route which shoul
2828
Here is a basic example of how to create a loader for a route, invoke a loader server function, fetch some data, and then access it in the route
2929

3030
```
31-
const loaderFn = createServerFn().handler(async () => {
32-
const segments = await getSegments();
33-
return { segments };
34-
});
31+
const loaderFn = createServerFn()
32+
.validator(
33+
z.object({
34+
title: z.string(),
35+
})
36+
)
37+
.handler(async () => {
38+
const segments = await getSegments();
39+
return { segments };
40+
});
3541

3642
export const Route = createFileRoute("/")({
3743
component: Home,
@@ -42,6 +48,42 @@ function Home() {
4248
const { segments } = Route.useLoaderData();
4349
```
4450

51+
## Validators
52+
53+
You should always add a validator to server functions.
54+
55+
```
56+
const createSegmentFn = createServerFn()
57+
.middleware([adminMiddleware])
58+
.validator(
59+
z.object({
60+
title: z.string(),
61+
content: z.string(),
62+
videoKey: z.string().optional(),
63+
slug: z.string(),
64+
moduleTitle: z.string(),
65+
length: z.string().optional(),
66+
isPremium: z.boolean(),
67+
})
68+
)
69+
.handler(async ({ data }) => {
70+
```
71+
72+
4573
# File Naming Convention
4674

4775
all files must be lowercase with hyphens.
76+
77+
# Data Access Prefernce
78+
79+
try to use the database.query method over doing manual select statements. here is an example of the query method: `await database.query.modules.findFirst(`
80+
81+
# Link component
82+
83+
never interpolate the to string, instead use props:
84+
85+
```
86+
<Link
87+
to="/learn/$slug"
88+
params={{ slug: continueSlug }}>Start Learning</Link>
89+
```

.env.sample

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ AWS_ACCESS_KEY_ID=S3RVER
88
AWS_SECRET_ACCESS_KEY=S3RVER
99
ADMIN_EMAIL=[email protected]
1010
STRIPE_SECRET_KEY=your_stripe_secret_key
11-
VITE_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key
1211
STRIPE_WEBHOOK_SECRET=your_stripe_webhook_secret
13-
STRIPE_PRICE_ID=your_stripe_price_id
12+
STRIPE_PRICE_ID=your_stripe_price_id
13+
UPLOAD_DIR=./files
14+
15+
VITE_STRIPE_PUBLISHABLE_KEY=your_stripe_publishable_key
16+
VITE_FILE_URL=http://localhost:3000/api/videos

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ yarn.lock
2020
/playwright-report/
2121
/blob-report/
2222
/playwright/.cache/
23-
s3/wdc-tanstack-starter-kit/
23+
s3/wdc-tanstack-starter-kit/
24+
files/*

app.config.ts

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,7 @@ import tsConfigPaths from "vite-tsconfig-paths";
33

44
export default defineConfig({
55
vite: {
6-
plugins: [
7-
tsConfigPaths({
8-
projects: ["./tsconfig.json"],
9-
}),
10-
],
11-
ssr: {
12-
noExternal: ["react-dropzone"],
13-
},
6+
plugins: [tsConfigPaths({ projects: ["./tsconfig.json"] })],
7+
ssr: { noExternal: ["react-dropzone"] },
148
},
159
});

app/components/ui/switch.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"use client";
2+
3+
import * as React from "react";
4+
import * as SwitchPrimitives from "@radix-ui/react-switch";
5+
6+
import { cn } from "~/lib/utils";
7+
8+
const Switch = React.forwardRef<
9+
React.ElementRef<typeof SwitchPrimitives.Root>,
10+
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
11+
>(({ className, ...props }, ref) => (
12+
<SwitchPrimitives.Root
13+
className={cn(
14+
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
15+
className
16+
)}
17+
{...props}
18+
ref={ref}
19+
>
20+
<SwitchPrimitives.Thumb
21+
className={cn(
22+
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
23+
)}
24+
/>
25+
</SwitchPrimitives.Root>
26+
));
27+
Switch.displayName = SwitchPrimitives.Root.displayName;
28+
29+
export { Switch };

app/data-access/modules.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { database } from "~/db";
2+
import { modules, segments } from "~/db/schema";
3+
import { eq } from "drizzle-orm";
4+
import type { Module, ModuleCreate } from "~/db/schema";
5+
6+
export async function getModules() {
7+
return database.select().from(modules).orderBy(modules.order);
8+
}
9+
10+
export async function getModuleById(id: Module["id"]) {
11+
const result = await database.query.modules.findFirst({
12+
where: eq(modules.id, id),
13+
});
14+
return result;
15+
}
16+
17+
export async function createModule(module: ModuleCreate) {
18+
const result = await database.insert(modules).values(module).returning();
19+
return result[0];
20+
}
21+
22+
export async function getModuleByTitle(title: string) {
23+
const result = await database
24+
.select()
25+
.from(modules)
26+
.where(eq(modules.title, title))
27+
.limit(1);
28+
return result[0];
29+
}
30+
31+
export async function getModulesWithSegments() {
32+
return database.query.modules.findMany({
33+
with: { segments: true },
34+
orderBy: modules.order,
35+
});
36+
}
37+
38+
export async function updateModuleOrder(moduleId: number, newOrder: number) {
39+
return database
40+
.update(modules)
41+
.set({ order: newOrder, updatedAt: new Date() })
42+
.where(eq(modules.id, moduleId))
43+
.returning();
44+
}
45+
46+
export async function reorderModules(updates: { id: number; order: number }[]) {
47+
// Use a transaction to ensure all updates happen together
48+
return database.transaction(async (tx) => {
49+
const results = [];
50+
for (const update of updates) {
51+
const [result] = await tx
52+
.update(modules)
53+
.set({ order: update.order, updatedAt: new Date() })
54+
.where(eq(modules.id, update.id))
55+
.returning();
56+
results.push(result);
57+
}
58+
return results;
59+
});
60+
}

app/data-access/segments.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { and, eq } from "drizzle-orm";
44
import type { Progress, Segment, SegmentCreate } from "~/db/schema";
55

66
export async function getSegments() {
7-
return database.select().from(segments).orderBy(segments.order);
7+
return database.query.segments.findMany();
88
}
99

1010
export type SegmentWithProgress = Segment & { progress: Progress | null };

app/db/schema.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,14 @@ export const sessions = tableCreator(
6060
(table) => [index("sessions_user_id_idx").on(table.userId)]
6161
);
6262

63+
export const modules = tableCreator("module", {
64+
id: serial("id").primaryKey(),
65+
title: text("title").notNull(),
66+
order: integer("order").notNull(),
67+
createdAt: timestamp("created_at").notNull().defaultNow(),
68+
updatedAt: timestamp("updated_at").notNull().defaultNow(),
69+
});
70+
6371
export const segments = tableCreator(
6472
"segment",
6573
{
@@ -70,7 +78,9 @@ export const segments = tableCreator(
7078
order: integer("order").notNull(),
7179
length: text("length"),
7280
isPremium: boolean("isPremium").notNull().default(false),
73-
moduleId: text("moduleId").notNull(),
81+
moduleId: serial("moduleId")
82+
.notNull()
83+
.references(() => modules.id, { onDelete: "cascade" }),
7484
videoKey: text("videoKey"),
7585
createdAt: timestamp("created_at").notNull().defaultNow(),
7686
updatedAt: timestamp("updated_at").notNull().defaultNow(),
@@ -98,8 +108,16 @@ export const progress = tableCreator(
98108
]
99109
);
100110

101-
export const segmentsRelations = relations(segments, ({ many }) => ({
111+
export const modulesRelations = relations(modules, ({ many }) => ({
112+
segments: many(segments),
113+
}));
114+
115+
export const segmentsRelations = relations(segments, ({ one, many }) => ({
102116
attachments: many(attachments),
117+
module: one(modules, {
118+
fields: [segments.moduleId],
119+
references: [modules.id],
120+
}),
103121
}));
104122

105123
export const attachments = tableCreator("attachment", {
@@ -128,3 +146,5 @@ export type Attachment = typeof attachments.$inferSelect;
128146
export type AttachmentCreate = typeof attachments.$inferInsert;
129147
export type Progress = typeof progress.$inferSelect;
130148
export type ProgressCreate = typeof progress.$inferInsert;
149+
export type Module = typeof modules.$inferSelect;
150+
export type ModuleCreate = typeof modules.$inferInsert;

0 commit comments

Comments
 (0)