Skip to content

Commit

Permalink
create channel and get members, channels endpoint and hook created , …
Browse files Browse the repository at this point in the history
…with create channel modal ui and jotai
  • Loading branch information
Diivvuu committed Oct 8, 2024
1 parent 108394d commit 3611f13
Show file tree
Hide file tree
Showing 15 changed files with 290 additions and 13 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
## todos
add reset password with email from convex password auth
add reset password with email from convex password auth

https://agile-toucan-670.convex.site
36 changes: 35 additions & 1 deletion convex/channels.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,41 @@
import { getAuthUserId } from "@convex-dev/auth/server";
import { query } from "./_generated/server";
import { mutation, query } from "./_generated/server";
import { v } from "convex/values";

export const create = mutation({
args: {
name: v.string(),
workspaceId: v.id("workspaces"),
},
handler: async (ctx, args) => {
const userId = await getAuthUserId(ctx);

if (!userId) {
throw new Error("Unauthorized");
}

const member = await ctx.db
.query("members")
.withIndex("by_workspace_id_user_id", (q) =>
q.eq("workspaceId", args.workspaceId).eq("userId", userId)
)
.unique();

if (!member || member.role !== "admin") {
throw new Error("Unauthorized");
}

const parsedName = args.name.replace(/\s+/g, "-").toLowerCase();

const channelId = await ctx.db.insert("channels", {
name: parsedName,
workspaceId: args.workspaceId,
});

return channelId;
},
});

export const get = query({
args: {
workspaceId: v.id("workspaces"),
Expand Down
3 changes: 2 additions & 1 deletion convex/users.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import { auth } from "./auth";
import { query } from "./_generated/server";
import { getAuthUserId } from "@convex-dev/auth/server";

export const current = query({
args: {},
handler: async (ctx) => {
const userId = await auth.getUserId(ctx);
const userId = await getAuthUserId(ctx);

if (userId === null) {
return null;
Expand Down
2 changes: 1 addition & 1 deletion src/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export default function Home() {
<div className="min-h-screen flex items-center justify-center">
{/* You are logged in user */}
{/* <Modals /> */}
{/* <UserButton /> */}
<UserButton />
</div>
);
}
56 changes: 56 additions & 0 deletions src/app/workspace/[workspaceId]/user-item.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import Link from "next/link";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import { Id } from "../../../../convex/_generated/dataModel";
import { cva, VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { useWorkspaceId } from "@/hooks/use-workspace-id";

const UserItemVariants = cva(
"flex items-center gap-1.5 justify-start font-normal h-7 px-4 text-sm overflow-hidden",
{
variants: {
variant: {
default: "text-[#f9edffcc]",
active: "text-[#481349] bg-white/90",
},
},
defaultVariants: {
variant: "default",
},
}
);
interface UserItemProps {
id: Id<"members">;
label?: string;
image?: string;
variant?: VariantProps<typeof UserItemVariants>["variant"];
}
export const UserItem = ({
id,
label = "Member",
image,
variant,
}: UserItemProps) => {
const workspaceId = useWorkspaceId();
const avatarFallback = label.charAt(0).toUpperCase();
console.log(id, label, image);
return (
<Button
variant="transparent"
className={cn(UserItemVariants({ variant: variant }))}
size="sm"
asChild
>
<Link href={`/workspace/${workspaceId}/member/${id}`}>
<Avatar className="size-5 rounded-md mr-1">
<AvatarImage className="rounded-md" src={image} />
<AvatarFallback className="rounded-md">
{avatarFallback}
</AvatarFallback>
</Avatar>
<span className="text-sm truncate">{label}</span>
</Link>
</Button>
);
};
34 changes: 32 additions & 2 deletions src/app/workspace/[workspaceId]/workspace-sidebar.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { useCurrentMember } from "@/features/members/api/use-current-member";
import { useGetWorkspace } from "@/features/workspaces/api/use-get-workspace";
import { useWorkspaceId } from "@/hooks/use-workspace-id";
import { useGetChannels } from "@/features/channels/api/use-get-channels";
import { useGetMembers } from "@/features/members/api/use-get-members";

import {
AlertTriangle,
Expand All @@ -12,11 +14,15 @@ import {

import { WorkspaceHeader } from "./workspace-header";
import { SidebarItem } from "./sidebar-item";
import { useGetChannels } from "@/features/channels/api/use-get-channels";
import { WorkspaceSection } from "./workspace-section";
import { UserItem } from "./user-item";
import { useCreateChannelModal } from "@/features/channels/store/use-create-channel-modal";

const WorkspaceSidebar = () => {
const workspaceId = useWorkspaceId();

const [_open, setOpen] = useCreateChannelModal();

const { data: member, isLoading: memberLoading } = useCurrentMember({
workspaceId,
});
Expand All @@ -26,6 +32,9 @@ const WorkspaceSidebar = () => {
const { data: channels, isLoading: channelsLoading } = useGetChannels({
workspaceId,
});
const { data: members, isLoading: membersLoading } = useGetMembers({
workspaceId,
});
if (workspaceLoading || memberLoading) {
return (
<div className="flex flex-col bg-[#5E2C5F] h-full items-center justify-center">
Expand All @@ -51,7 +60,13 @@ const WorkspaceSidebar = () => {
<SidebarItem label="Threads" icon={MessageSquareText} id="threads" />
<SidebarItem label="Drafts & Sent" icon={SendHorizonal} id="drafts" />
</div>
<WorkspaceSection label="Channels" hint="New Channel" onNew={() => {}}>
<WorkspaceSection
label="Channels"
hint="New Channel"
onNew={() => {
member.role === "admin" ? setOpen(true) : undefined;
}}
>
{channels?.map((item) => (
<SidebarItem
key={item._id}
Expand All @@ -61,6 +76,21 @@ const WorkspaceSidebar = () => {
/>
))}
</WorkspaceSection>
<WorkspaceSection
label="Direct Messages"
hint="New Direct Message"
onNew={() => {}}
>
{members?.map((item) => (
<UserItem
key={item._id}
id={item._id}
label={item.user.name}
image={item.user.image}
// variant={item._id === }
/>
))}
</WorkspaceSection>
</div>
);
};
Expand Down
2 changes: 2 additions & 0 deletions src/components/modals.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"use client";
import { CreateChannelModal } from "@/features/channels/components/create-channel-modal";
import { CreateWorkSpaceModal } from "@/features/workspaces/components/create-workspace-modal";
import { useEffect, useState } from "react";

Expand All @@ -10,6 +11,7 @@ export const Modals = () => {
if (!mounted) return null;
return (
<>
<CreateChannelModal />
<CreateWorkSpaceModal />
</>
);
Expand Down
1 change: 0 additions & 1 deletion src/features/auth/components/sign-in-card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export const SignInCard = ({ setState }: SignInCardProps) => {
};

const handleProviderSignIn = (value: "github" | "google") => {
console.log("clivked");
setPending(true);
signIn(value).finally(() => {
setPending(false);
Expand Down
4 changes: 2 additions & 2 deletions src/features/auth/components/user-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@ export const UserButton = () => {
if (!data) {
return null;
}

const { image, name, email } = data;

const avatarFallback = name!.charAt(0).toUpperCase();
return (
<DropdownMenu modal={false}>
<DropdownMenuTrigger className="outline-none relative">
<Avatar className="size-10 hover:opacity-75 transition">
{/* <AvatarImage alt="name" src={image} /> */}
<AvatarImage alt="name" src={image} />
<AvatarFallback>{avatarFallback}</AvatarFallback>
</Avatar>
</DropdownMenuTrigger>
Expand Down
63 changes: 63 additions & 0 deletions src/features/channels/api/use-create-channel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useCallback, useMemo, useState } from "react";
import { Id } from "../../../../convex/_generated/dataModel";
import { useMutation } from "convex/react";
import { api } from "../../../../convex/_generated/api";
import { isSet } from "util/types";

type RequestType = { name: string; workspaceId: Id<"workspaces"> };
type ResponseType = Id<"channels"> | null;

type Options = {
onSuccess?: (data: ResponseType) => void;
onError?: (error: Error) => void;
onSettled?: () => void;
throwError?: boolean;
};

export const useCreateChannel = () => {
const [data, setData] = useState<ResponseType>(null);
const [error, setError] = useState<Error | null>(null);
const [status, setStatus] = useState<
"success" | "error" | "settled" | "pending" | null
>(null);

const isPending = useMemo(() => status === "pending", [status]);
const isSuccess = useMemo(() => status === "success", [status]);
const isError = useMemo(() => status === "error", [status]);
const isSettled = useMemo(() => status === "settled", [status]);

const mutation = useMutation(api.channels.create);

const mutate = useCallback(
async (values: RequestType, options?: Options) => {
try {
setData(null);
setError(null);
setStatus("pending");
const response = await mutation(values);
options?.onSuccess?.(response);
setStatus("success");
return response;
} catch (error) {
options?.onError?.(error as Error);
setStatus("error");
if (options?.throwError) {
throw error;
}
} finally {
setStatus("settled");
options?.onSettled?.();
}
},
[mutation]
);
return {
mutate,
data,
error,
isPending,
isSuccess,
isError,
isSettled,
};
};
1 change: 0 additions & 1 deletion src/features/channels/api/use-get-members.ts

This file was deleted.

69 changes: 69 additions & 0 deletions src/features/channels/components/create-channel-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { useCreateChannelModal } from "../store/use-create-channel-modal";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import React, { useState } from "react";
import { useCreateChannel } from "../api/use-create-channel";
import { useWorkspaceId } from "@/hooks/use-workspace-id";

export const CreateChannelModal = () => {
const workspaceId = useWorkspaceId();
const { mutate, isPending } = useCreateChannel();

const [open, setOpen] = useCreateChannelModal();
const [name, setName] = useState("");

const handleClose = () => {
setName("");
setOpen(false);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
//space here automatic turn to -
const value = e.target.value.replace(/\s+/g, "-").toLocaleLowerCase();
setName(value);
};
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
mutate(
{ name, workspaceId },
{
onSuccess: (id) => {
//todo : redirect to that channel
handleClose();
},
}
);
};

return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent>
<DialogHeader>
<DialogTitle>Add a channel</DialogTitle>
</DialogHeader>
<form className="space-y-4" onSubmit={handleSubmit}>
<Input
value={name}
disabled={isPending}
onChange={handleChange}
required
autoFocus
minLength={3}
maxLength={80}
placeholder="e.g. plan-budget"
/>
<div className="flex justify-end">
<Button className="" disabled={false}>
Create
</Button>
</div>
</form>
</DialogContent>
</Dialog>
);
};
7 changes: 7 additions & 0 deletions src/features/channels/store/use-create-channel-modal.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { atom, useAtom } from "jotai";

const modalState = atom(false);

export const useCreateChannelModal = () => {
return useAtom(modalState);
};
Loading

0 comments on commit 3611f13

Please sign in to comment.