Skip to content

Commit

Permalink
Connect Questions to Video
Browse files Browse the repository at this point in the history
  • Loading branch information
shaurya35 committed Sep 28, 2024
1 parent b0b9869 commit d98bd9f
Show file tree
Hide file tree
Showing 7 changed files with 191 additions and 25 deletions.
4 changes: 2 additions & 2 deletions src/actions/question/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ const createQuestionHandler = async (
};
}

const { title, content, tags } = data;
const { title, content, tags, videoId = 0 } = data;

// Create initial slug
let slug = generateHandle(title);
Expand Down Expand Up @@ -89,7 +89,7 @@ const updateQuestionHandler = async (
};
}

const { title, content, tags, questionId } = data;
const { title, content, tags, questionId, videoId = 0 } = data;
const userExists = await db.user.findUnique({
where: { id: session.user.id },
});
Expand Down
1 change: 1 addition & 0 deletions src/app/(main)/(pages)/question/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ const getQuestionsWithQuery = async (
slug: true,
createdAt: true,
updatedAt: true,
videoId: true,
votes: {
where: { userId: sessionId },
select: { userId: true, voteType: true },
Expand Down
4 changes: 2 additions & 2 deletions src/components/CourseView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,8 @@ export const CourseView = ({
? 'folder'
: courseContent?.value.type;
return (
<div className="flex w-full flex-col gap-8 pb-16 pt-8 xl:pt-[9px]">
<div className="flex flex-col gap-4 xl:pt-44">
<div className="flex w-full flex-col gap-8 pb-16 pt-8 xl:pt-[10px] xl:mr-12">
<div className="flex flex-col gap-4 xl:ml-44 ">
<BreadCrumbComponent
course={course}
contentType={contentType}
Expand Down
152 changes: 139 additions & 13 deletions src/components/NewPostDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ import { FormPostInput } from './posts/form/form-input';
import { FormPostErrors } from './posts/form/form-errors';
import { X } from 'lucide-react';

interface Video {
id: number;
title: string;
}

export const NewPostDialog = () => {
const { theme } = useTheme();
const formRef = useRef<ElementRef<'form'>>(null);
Expand All @@ -29,8 +34,73 @@ export const NewPostDialog = () => {
const tagInputRef = useRef<HTMLInputElement | null>(null);
const [value, setValue] = useState<string>('**Hello world!!!**');
const [tags, setTags] = useState<string[]>([]);
const [videos, setVideos] = useState<any[]>([]);
const [videoTitleValue, setVideoTitleValue] = useState('');
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const [search, setSearch] = useState('');
const [selectedVideo, setSelectedVideo] = useState<Video | null>(null);
const videoTitleInputRef = useRef<HTMLInputElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
const { ref, onOpen, onClose } = useModal();

useEffect(() => {
const fetchVideos = async () => {
try {
const response = await fetch('/api/search?q=videos');
const data = await response.json();
//dummy data to test
// const data = [
// { id: 1, title: 'jwt and authentication' },
// { id: 2, title: 'Zod and validation' },
// { id: 3, title: 'Validation Complete' },
// { id: 4, title: 'Introduction to react' },
// { id: 5, title: 'useState, useEffect' },
// { id: 6, title: 'useRef, useMemo' },
// { id: 7, title: 'React Hooks' },
// { id: 8, title: 'React Context and summary' },
// ];

setVideos(data);
} catch (error) {
console.error('Error fetching videos:', error);
}
};

fetchVideos();
}, []);

const handleInputClick = () => {
setIsDropdownOpen((prevOpen) => !prevOpen);
};

useEffect(() => {
const handleOutsideClick = (event: MouseEvent) => {
if (
!containerRef.current?.contains(event.target as Node) &&
isDropdownOpen
) {
setIsDropdownOpen(false);
}
};

document.addEventListener('click', handleOutsideClick);

return () => {
document.removeEventListener('click', handleOutsideClick);
};
}, [containerRef, isDropdownOpen]);

const handleVideoSelect = (video: Video) => {
setSelectedVideo(video);
setIsDropdownOpen(false);
setVideoTitleValue(video.title);
};

const handleRemoveVideo = () => {
setSelectedVideo(null);
setVideoTitleValue('');
};

const handleMarkdownChange = (newValue?: string) => {
if (typeof newValue === 'string') {
setValue(newValue);
Expand All @@ -40,8 +110,6 @@ export const NewPostDialog = () => {
let timeoutId: any;
if (paramsObject.newPost === 'open') {
onOpen();

// Cleanup function to clear the timeout
} else {
onClose();
}
Expand All @@ -56,7 +124,9 @@ export const NewPostDialog = () => {
formRef?.current?.reset();
setValue('');
router.push(`/question/${data.slug}`);
setTags([]);
setTags([]);
setSelectedVideo(null);
setVideoTitleValue('');
handleOnCloseClick();
},
onError: (error) => {
Expand All @@ -76,31 +146,32 @@ export const NewPostDialog = () => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const title = formData.get('title');
const videoId = selectedVideo?.id;
execute({
title: title?.toString() || '',
content: value,
tags,
videoId,
});
setVideoTitleValue('');
setSelectedVideo(null);
handleOnCloseClick();
};

const addTag = (event: React.KeyboardEvent<HTMLInputElement>) => {
if (event.key === ',') {
event.preventDefault();
const formData = new FormData(formRef.current as HTMLFormElement);
const tag = formData.get('tags')?.toString().trim().replace(/,+$/, '');

if (tag) {
setTags((prevTags) => [
...prevTags,
tag
]);
setTags((prevTags) => [...prevTags, tag]);
}
if (tagInputRef.current) {
tagInputRef.current.value = '';
}
}
};


const removeTag = (tag: string) => {
setTags(tags.filter((t) => t !== tag));
Expand Down Expand Up @@ -180,17 +251,72 @@ export const NewPostDialog = () => {
/>
</div>
</div>

<div className="flex w-full flex-col gap-2">
<h3 className="wmde-markdown-var text-lg font-bold tracking-tighter">
Link Video
</h3>
<FormPostInput
id="title"
placeholder="Link a Related Video"
id="video-search"
placeholder="Search a video to tag"
errors={fieldErrors}
onClick={handleInputClick}
className="w-full"
value={selectedVideo?.title || videoTitleValue}
onChange={(e) => {
setVideoTitleValue(e.target.value);
if (e.target.value === '') {
setSelectedVideo(null);
}
}}
disabled={selectedVideo !== null}
ref={videoTitleInputRef}
/>

{selectedVideo && (
<button
className="ml-2 cursor-pointer"
onClick={handleRemoveVideo}
>
Remove
</button>
)}
{isDropdownOpen && (
<div
ref={containerRef}
className="h-32 overflow-y-auto rounded-lg bg-gray-200 dark:bg-[#15161D]"
>
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Search videos"
className="w-full px-4 py-2"
/>
{videos
.filter((video) =>
video.title
.toLowerCase()
.includes(search.toLowerCase()),
)
.map((video) => (
<div key={video.id}>
<a
className="block cursor-pointer rounded-lg px-4 py-2 text-white dark:text-white"
style={{
backgroundColor: 'var(--background)',
color: 'var(--text)',
}}
onClick={() => {
handleVideoSelect(video);
setSearch('');
}}
>
{video.title}
</a>
</div>
))}
</div>
)}
</div>
<div
data-color-mode={theme}
Expand All @@ -217,4 +343,4 @@ export const NewPostDialog = () => {
</AnimatePresence>
</Modal>
);
};
};
15 changes: 10 additions & 5 deletions src/components/admin/ContentRendererClient.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,16 @@ export const ContentRendererClient = ({
<h2 className="line-clamp-2 text-wrap text-2xl font-extrabold capitalize tracking-tight text-primary md:text-3xl">
{content.title}
</h2>
{metadata.slides ? (
<Link href={metadata.slides} target="_blank">
<Button className="gap-2">Lecture Slides</Button>
<div className="flex gap-2">
<Link href={`/question?videoId=${content.id}`}>
<Button className="gap-2">View related questions</Button>
</Link>
) : null}
{metadata.slides ? (
<Link href={metadata.slides} target="_blank">
<Button className="gap-2">Lecture Slides</Button>
</Link>
) : null}
</div>
</div>

{!showChapters && metadata.segments?.length > 0 && (
Expand Down Expand Up @@ -157,4 +162,4 @@ export const ContentRendererClient = ({
</div>
</div>
);
};
};
29 changes: 27 additions & 2 deletions src/components/posts/PostCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client';
import '@uiw/react-md-editor/markdown-editor.css';
import '@uiw/react-markdown-preview/markdown.css';
import React, { useState, useTransition } from 'react';
import React, { useState, useTransition, useEffect } from 'react';
import VoteForm from './form/form-vote';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
Expand Down Expand Up @@ -54,6 +54,7 @@ const PostCard: React.FC<IProps> = ({
const { theme } = useTheme();
const [markDownValue, setMarkDownValue] = useState('');
const [enableReply, setEnableReply] = useState(false);

const handleMarkdownChange = (newValue?: string) => {
if (typeof newValue === 'string') {
setMarkDownValue(newValue);
Expand All @@ -62,7 +63,7 @@ const PostCard: React.FC<IProps> = ({

const router = useRouter();

const [isPending, startTransition] = useTransition();
const [isPending, startTransition] = useTransition();

const { execute, fieldErrors } = useAction(createAnswer, {
onSuccess: () => {
Expand Down Expand Up @@ -93,6 +94,24 @@ const PostCard: React.FC<IProps> = ({
return num.toString();
};

const [videoTitles, setVideoTitles] = useState({});

useEffect(() => {
const fetchVideoTitles = async () => {
const response = await fetch('/api/search?q=videos');
const data = await response.json();
const videoTitlesMap = data.reduce(
(acc: { [x: string]: any }, video: { id: number; title: string }) => {
acc[video.id] = video.title;
return acc;
},
{},
);
setVideoTitles(videoTitlesMap);
};
fetchVideoTitles();
}, []);

return (
<div
className={`flex w-full cursor-pointer flex-col gap-4 p-3 transition-all duration-300 sm:p-5 ${
Expand Down Expand Up @@ -179,6 +198,12 @@ const PostCard: React.FC<IProps> = ({
</div>
)}

{isExtendedQuestion(post) && post.videoId && (
<p className="text-xs tracking-tight text-primary/70 sm:text-sm">
Video: {videoTitles[post.videoId] || post.videoId}
</p>
)}

<div className="flex flex-wrap items-center gap-2">
<VoteForm
upvotes={post.upvotes}
Expand Down
11 changes: 10 additions & 1 deletion src/components/posts/form/form-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ interface FormInputProps {
errors?: Record<string, string[] | undefined>;
className?: string;
defaultValue?: string;
value?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
onClick?:() => void;
onBlur?: () => void;
onKeyUp?: (event: React.KeyboardEvent<HTMLInputElement>) => void;
}
Expand All @@ -35,6 +38,9 @@ export const FormPostInput = forwardRef<HTMLInputElement, FormInputProps>(
errors,
className,
defaultValue = '',
onClick,
value,
onChange,
onKeyUp,
onBlur,
},
Expand All @@ -55,11 +61,13 @@ export const FormPostInput = forwardRef<HTMLInputElement, FormInputProps>(
) : null}
<Input
onBlur={onBlur}
defaultValue={defaultValue}
ref={ref}
required={required}
name={id}
id={id}
onClick={onClick}
value={value}
onChange={onChange}
onKeyUp={onKeyUp}
placeholder={placeholder}
type={type}
Expand All @@ -69,6 +77,7 @@ export const FormPostInput = forwardRef<HTMLInputElement, FormInputProps>(
className,
)}
aria-describedby={`${id}-error`}
defaultValue={value === undefined ? defaultValue : undefined}
/>
</div>
<FormPostErrors id={id} errors={errors} />
Expand Down

0 comments on commit d98bd9f

Please sign in to comment.