Skip to content

Commit d0c2eb4

Browse files
authored
Merge pull request #32 from duylongpro99/development
Development
2 parents 390ea17 + 0f61b3a commit d0c2eb4

File tree

7 files changed

+184
-133
lines changed

7 files changed

+184
-133
lines changed

.github/workflows/build-reusable.yml

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,19 @@ name: Build Reusable Steps
33
on:
44
workflow_call:
55
outputs:
6-
build_output: # Changed from build-output to build_output for consistency
6+
build_output: # Changed from build-output to build_output for consistency
77
description: "Build output artifact"
88
value: ${{ jobs.build.outputs.build_output }}
99
jobs:
1010
build:
1111
name: "Build Application"
1212
runs-on: ubuntu-latest
13+
env:
14+
NEXT_PUBLIC_CONVEX_URL: ${{ secrets.NEXT_PUBLIC_CONVEX_URL }}
15+
CONVEX_DEPLOYMENT: ${{ secrets.CONVEX_DEPLOYMENT }}
16+
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
17+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
18+
LIVE_BLOCK_SECRET_API_KEY: ${{ secrets.LIVE_BLOCK_SECRET_API_KEY }}
1319
outputs:
1420
build_output: ${{ steps.build_step.outputs.result }}
1521

@@ -55,32 +61,28 @@ jobs:
5561
pnpm install
5662
fi
5763
- name: Build
58-
id: build_step # Added this ID which is referenced in the outputs
64+
id: build_step # Added this ID which is referenced in the outputs
5965
env:
6066
NEXT_PUBLIC_CONVEX_URL: ${{ secrets.NEXT_PUBLIC_CONVEX_URL }}
6167
CONVEX_DEPLOYMENT: ${{ secrets.CONVEX_DEPLOYMENT }}
6268
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}
6369
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ secrets.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY }}
6470
LIVE_BLOCK_SECRET_API_KEY: ${{ secrets.LIVE_BLOCK_SECRET_API_KEY }}
6571
run: |
66-
# Create a clean .env file
67-
rm -f .env
68-
touch .env
69-
70-
# Add each environment variable to .env file
71-
# Using printf to avoid issues with special characters
72-
printf "NEXT_PUBLIC_CONVEX_URL=%s\n" "$NEXT_PUBLIC_CONVEX_URL" >> .env
73-
printf "CONVEX_DEPLOYMENT=%s\n" "$CONVEX_DEPLOYMENT" >> .env
74-
printf "CLERK_SECRET_KEY=%s\n" "$CLERK_SECRET_KEY" >> .env
75-
printf "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=%s\n" "$NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY" >> .env
76-
printf "LIVE_BLOCK_SECRET_API_KEY=%s\n" "$LIVE_BLOCK_SECRET_API_KEY" >> .env
77-
78-
72+
# Create .env file more efficiently (single operation)
73+
cat > .env << EOL
74+
NEXT_PUBLIC_CONVEX_URL=${NEXT_PUBLIC_CONVEX_URL}
75+
CONVEX_DEPLOYMENT=${CONVEX_DEPLOYMENT}
76+
CLERK_SECRET_KEY=${CLERK_SECRET_KEY}
77+
NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=${NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY}
78+
LIVE_BLOCK_SECRET_API_KEY=${LIVE_BLOCK_SECRET_API_KEY}
79+
EOL
80+
7981
# Run the build command
80-
pnpm build
81-
82-
# Set output for the workflow
83-
echo "result=success" >> $GITHUB_OUTPUT
82+
pnpm build || exit 1 # Add error handling
83+
84+
# Set output for the workflow more securely
85+
echo "result=success" >> "$GITHUB_OUTPUT"
8486
8587
- name: Upload build artifacts
8688
uses: actions/upload-artifact@v4
@@ -92,4 +94,4 @@ jobs:
9294
package.json
9395
pnpm-lock.yaml
9496
next.config.js
95-
.env
97+
.env

liveblocks.config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ declare global {
1919
id: string;
2020
info: {
2121
// Example properties, for useSelf, useUser, useOthers, etc.
22-
// name: string;
23-
// avatar: string;
22+
name: string;
23+
avatar: string;
2424
};
2525
};
2626

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,19 @@
11
import { auth, currentUser } from "@clerk/nextjs/server";
2-
// import { Liveblocks } from "@liveblocks/node";
3-
// import { ConvexHttpClient } from "convex/browser";
4-
// import { api } from "../../../../../convex/_generated/api";
5-
6-
// const convex = new ConvexHttpClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
7-
// const liveblocks = new Liveblocks({
8-
// secret: process.env.LIVE_BLOCK_SECRET_API_KEY!,
9-
// });
2+
import { Liveblocks } from "@liveblocks/node";
3+
import { ConvexHttpClient } from "convex/browser";
4+
import { api } from "../../../../../convex/_generated/api";
5+
6+
// Ensure these environment variables are defined in your GitHub Actions workflow
7+
// Add error handling for missing environment variables
8+
const convex = new ConvexHttpClient(
9+
process.env.NEXT_PUBLIC_CONVEX_URL ??
10+
(() => { throw new Error("NEXT_PUBLIC_CONVEX_URL environment variable is not defined") })()
11+
);
12+
13+
const liveblocks = new Liveblocks({
14+
secret: process.env.LIVE_BLOCK_SECRET_API_KEY ??
15+
(() => { throw new Error("LIVE_BLOCK_SECRET_API_KEY environment variable is not defined") })()
16+
});
1017

1118
export async function POST(req: Request) {
1219
const { sessionClaims } = await auth();
@@ -16,31 +23,30 @@ export async function POST(req: Request) {
1623
if (!user) return new Response("Unauthorized", { status: 401 });
1724

1825
const { room } = await req.json();
19-
// const document = await convex.query(api.document.get, {
20-
// id: room,
21-
// ignoreAuth: true,
22-
// });
23-
24-
// if (!document) return new Response("Unauthorized", { status: 401 });
25-
26-
// const isOwner = document.ownerId === user.id;
27-
// const isOrgMember = !!(
28-
// document.organizationId && document.organizationId === sessionClaims.org_id
29-
// );
30-
31-
// if (!isOwner && !isOrgMember)
32-
// return new Response("Unauthorized", { status: 401 });
33-
34-
// const session = liveblocks.prepareSession(user.id, {
35-
// userInfo: {
36-
// name:
37-
// user.fullName ?? user.primaryEmailAddress?.emailAddress ?? "Anonymous",
38-
// avatar: user.imageUrl,
39-
// },
40-
// });
41-
42-
// session.allow(room, session.FULL_ACCESS);
43-
// const { body, status } = await session.authorize();
44-
// return new Response(body, { status });
45-
return new Response(room, { status: 200 });
26+
const document = await convex.query(api.document.get, {
27+
id: room,
28+
ignoreAuth: true,
29+
});
30+
31+
if (!document) return new Response("Unauthorized", { status: 401 });
32+
33+
const isOwner = document.ownerId === user.id;
34+
const isOrgMember = !!(
35+
document.organizationId && document.organizationId === sessionClaims.org_id
36+
);
37+
38+
if (!isOwner && !isOrgMember)
39+
return new Response("Unauthorized", { status: 401 });
40+
41+
const session = liveblocks.prepareSession(user.id, {
42+
userInfo: {
43+
name:
44+
user.fullName ?? user.primaryEmailAddress?.emailAddress ?? "Anonymous",
45+
avatar: user.imageUrl,
46+
},
47+
});
48+
49+
session.allow(room, session.FULL_ACCESS);
50+
const { body, status } = await session.authorize();
51+
return new Response(body, { status });
4652
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
"use client";
2+
3+
import { useOthers, useSelf } from "@liveblocks/react/suspense";
4+
import { Avatars } from "./avatars";
5+
6+
export const AvatarsStack: React.FC = () => {
7+
const users = useOthers();
8+
const currentUser = useSelf();
9+
10+
if (!users.length) return <></>;
11+
return (
12+
<>
13+
<div className="items-center">
14+
{currentUser && (
15+
<div className="relative ml-2">
16+
<Avatars name="You" src={currentUser.info.avatar} />
17+
</div>
18+
)}
19+
</div>
20+
</>
21+
);
22+
};
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import Image from "next/image";
2+
import React from "react";
3+
4+
const SIZE = 36;
5+
6+
type Props = {
7+
src: string;
8+
name: string;
9+
};
10+
11+
export const Avatars: React.FC<Props> = ({ src, name }) => {
12+
return (
13+
<>
14+
<div
15+
style={{ width: SIZE, height: SIZE }}
16+
className="group -ml-2 flex shrink-0 place-content-center relative border-4 border-white rounded-full bg-gray-400"
17+
>
18+
<div className="opacity-0 group-hover:opacity-100 absolute top-full py-1 px-2 text-white text-xs rounded-lg mt-2.5 z-10 bg-black whitespace-nowrap transition-opacity">
19+
{name}
20+
</div>
21+
<Image src={src} alt={name} className="size-full rounded-full" />
22+
</div>
23+
</>
24+
);
25+
};
Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Navbar } from "./(navbar)/navbar";
22
import { Toolbar } from "./(toolbar)/toolbar";
33
import { Editor } from "./editor";
4-
// import { Room } from "./room";
4+
import { Room } from "./room";
55

66
interface Props {
77
params: Promise<{ documentId: string }>;
@@ -10,22 +10,20 @@ const Page: React.FC<Props> = async ({ params }) => {
1010
const { documentId } = await params;
1111
console.info(":Document:", documentId);
1212

13-
// <Room>
1413
return (
15-
<div className="min-h-screen bg-[#fafbfd]">
16-
<div className="flex flex-col px-4 pt-2 gap-y-2 fixed top-0 left-0 right-0 z-10 bg-[#fafbfd] print:hidden">
17-
<Navbar />
18-
<Toolbar />
19-
</div>
14+
<Room>
15+
<div className="min-h-screen bg-[#fafbfd]">
16+
<div className="flex flex-col px-4 pt-2 gap-y-2 fixed top-0 left-0 right-0 z-10 bg-[#fafbfd] print:hidden">
17+
<Navbar />
18+
<Toolbar />
19+
</div>
2020

21-
<div className="pt-[114px] print:pt-0">
22-
<Editor />
21+
<div className="pt-[114px] print:pt-0">
22+
<Editor />
23+
</div>
2324
</div>
24-
</div>
25+
</Room>
2526
);
26-
{
27-
/* </Room> */
28-
}
2927
};
3028

3129
export default Page;
Lines changed: 61 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,73 @@
11
"use client";
22

3-
import { ReactNode } from "react";
3+
import { FullscreenLoader } from "@/components/fullscreen-loader";
4+
import { useToast } from "@/hooks/use-toast";
5+
import {
6+
ClientSideSuspense,
7+
LiveblocksProvider,
8+
RoomProvider,
9+
} from "@liveblocks/react/suspense";
10+
import { useParams } from "next/navigation";
11+
import { ReactNode, useEffect, useMemo, useState } from "react";
12+
import { getUsers } from "./action";
413

5-
// import { FullscreenLoader } from "@/components/fullscreen-loader";
6-
// import { useToast } from "@/hooks/use-toast";
7-
// import {
8-
// ClientSideSuspense,
9-
// LiveblocksProvider,
10-
// RoomProvider,
11-
// } from "@liveblocks/react/suspense";
12-
// import { useParams } from "next/navigation";
13-
// import { ReactNode, useEffect, useMemo, useState } from "react";
14-
// import { getUsers } from "./action";
15-
16-
// type User = {
17-
// id: string;
18-
// name: string;
19-
// avatar: string;
20-
// };
14+
type User = {
15+
id: string;
16+
name: string;
17+
avatar: string;
18+
};
2119

2220
export function Room({ children }: { children: ReactNode }) {
23-
// const params = useParams<{ documentId: string }>();
24-
// const { toast } = useToast();
21+
const params = useParams<{ documentId: string }>();
22+
const { toast } = useToast();
2523

26-
// const [users, setUsers] = useState<User[]>([]);
24+
const [users, setUsers] = useState<User[]>([]);
2725

28-
// const fetchUsers = useMemo(
29-
// () => async () => {
30-
// try {
31-
// const users = await getUsers();
32-
// setUsers(users);
33-
// } catch {
34-
// toast({
35-
// title: "Error",
36-
// description: "Failed to fetch users",
37-
// variant: "destructive",
38-
// });
39-
// }
40-
// },
41-
// [toast]
42-
// );
26+
const fetchUsers = useMemo(
27+
() => async () => {
28+
try {
29+
const users = await getUsers();
30+
setUsers(users);
31+
} catch {
32+
toast({
33+
title: "Error",
34+
description: "Failed to fetch users",
35+
variant: "destructive",
36+
});
37+
}
38+
},
39+
[toast]
40+
);
4341

44-
// useEffect(() => {
45-
// fetchUsers();
46-
// }, [fetchUsers]);
42+
useEffect(() => {
43+
fetchUsers();
44+
}, [fetchUsers]);
4745

4846
return (
49-
// <LiveblocksProvider
50-
// authEndpoint={"/api/auth/liveblocks"}
51-
// throttle={16}
52-
// resolveUsers={({ userIds }) =>
53-
// userIds.map((id) => users.find((u) => u.id === id))
54-
// }
55-
// resolveMentionSuggestions={({ text }) => {
56-
// let filterUsers = users;
57-
// if (text) {
58-
// filterUsers = users.filter((u) =>
59-
// u.name.toLowerCase().includes(text.toLowerCase())
60-
// );
61-
// }
62-
// return filterUsers.map((u) => u.id);
63-
// }}
64-
// resolveRoomsInfo={() => []}
65-
// >
66-
// <RoomProvider id={params.documentId}>
67-
// <ClientSideSuspense
68-
// fallback={<FullscreenLoader label="Document loading..." />}
69-
// >
70-
<>{children}</>
71-
// </ClientSideSuspense>
72-
// </RoomProvider>
73-
// </LiveblocksProvider>
47+
<LiveblocksProvider
48+
authEndpoint={"/api/auth/liveblocks"}
49+
throttle={16}
50+
resolveUsers={({ userIds }) =>
51+
userIds.map((id) => users.find((u) => u.id === id))
52+
}
53+
resolveMentionSuggestions={({ text }) => {
54+
let filterUsers = users;
55+
if (text) {
56+
filterUsers = users.filter((u) =>
57+
u.name.toLowerCase().includes(text.toLowerCase())
58+
);
59+
}
60+
return filterUsers.map((u) => u.id);
61+
}}
62+
resolveRoomsInfo={() => []}
63+
>
64+
<RoomProvider id={params.documentId}>
65+
<ClientSideSuspense
66+
fallback={<FullscreenLoader label="Document loading..." />}
67+
>
68+
{children}
69+
</ClientSideSuspense>
70+
</RoomProvider>
71+
</LiveblocksProvider>
7472
);
7573
}

0 commit comments

Comments
 (0)