Skip to content

Commit eed7b69

Browse files
feat: added releases notes page
1 parent dacc9b4 commit eed7b69

File tree

13 files changed

+444
-41
lines changed

13 files changed

+444
-41
lines changed

.env.example

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,9 @@ UPLOADTHING_SECRET=
4949
# Pusher
5050
PUSHER_APP_ID=
5151
PUSHER_SECRET=
52+
53+
# Release notes
54+
GITHUB_OWNER=
55+
GITHUB_REPO=
56+
GITHUB_TOKEN=
57+

actions/github/get-releases.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
'use server';
2+
3+
import { ONE_DAY_SEC } from '@/constants/common';
4+
import { PAGE_SIZES } from '@/constants/paginations';
5+
import { fetchCachedData } from '@/lib/cache';
6+
import { fetcher } from '@/lib/fetcher';
7+
8+
type GetGithubReleases = {
9+
pageIndex?: string | number;
10+
pageSize?: string | number;
11+
};
12+
13+
export const getGithubReleases = async ({
14+
pageIndex = 0,
15+
pageSize = PAGE_SIZES[0],
16+
}: GetGithubReleases): Promise<
17+
{
18+
body: string | null;
19+
html_url: string | null;
20+
name: string | null;
21+
publishedAt: string | null;
22+
zipUrl: string | null;
23+
}[]
24+
> => {
25+
try {
26+
const githubReleases = await fetchCachedData(
27+
'github-releases',
28+
async () => {
29+
const res = await fetcher.get(
30+
`https://api.github.com/repos/${process.env.GITHUB_OWNER}/${process.env.GITHUB_REPO}/releases?per_page=${pageSize}&page=${pageIndex}`,
31+
{
32+
responseType: 'json',
33+
headers: {
34+
Accept: 'application/vnd.github+json',
35+
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
36+
},
37+
},
38+
);
39+
40+
return res.map((release: any) => ({
41+
body: release.body,
42+
html_url: release.html_url,
43+
name: release.name,
44+
publishedAt: release.published_at,
45+
zipUrl: release.zipball_url,
46+
}));
47+
},
48+
ONE_DAY_SEC,
49+
);
50+
51+
return githubReleases;
52+
} catch (error) {
53+
console.error('[GET_GITHUB_RELEASES]', error);
54+
55+
return [];
56+
}
57+
};

app/(chat)/(routes)/chat/_components/chat-bubble.tsx

Lines changed: 2 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
'use client';
22

3-
import Markdown from 'react-markdown';
4-
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
5-
import { atomDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
6-
3+
import { MarkdownText } from '@/components/common/markdown-text';
74
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui';
85
import { ChatCompletionRole } from '@/constants/open-ai';
96
import { getFallbackName } from '@/lib/utils';
@@ -37,33 +34,7 @@ export const ChatBubble = ({ message, name, picture, streamMessage }: ChatBubble
3734
<div className="space-x-2">
3835
<span className="text-medium font-bold">{name}</span>
3936
</div>
40-
<p className="text-sm prose dark:prose-invert prose-a:text-accent-primary prose-a:no-underline hover:prose-a:underline prose-pre:bg-transparent">
41-
<Markdown
42-
components={{
43-
code({ inline, className, children, ...props }: any) {
44-
const match = /language-(\w+)/.exec(className || '');
45-
46-
return !inline && match ? (
47-
<SyntaxHighlighter
48-
{...props}
49-
PreTag="div"
50-
language={match[1]}
51-
style={atomDark}
52-
wrapLongLines
53-
>
54-
{String(children).replace(/\n$/, '')}
55-
</SyntaxHighlighter>
56-
) : (
57-
<code {...props} className={className || ''}>
58-
{children}
59-
</code>
60-
);
61-
},
62-
}}
63-
>
64-
{streamMessage ?? message.content}
65-
</Markdown>
66-
</p>
37+
<MarkdownText text={streamMessage ?? message.content} />
6738
</div>
6839
</div>
6940
</div>

app/(dashboard)/(routes)/teacher/courses/[courseId]/_components/form/description-form.tsx

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { useForm } from 'react-hook-form';
99
import toast from 'react-hot-toast';
1010
import * as z from 'zod';
1111

12-
import { GenerateDescriptionAi } from '@/components/ai/generate-description-ai';
12+
import { GenerateTextResponseAi } from '@/components/ai/generate-text-response-ai';
1313
import { Button } from '@/components/ui/button';
1414
import {
1515
Form,
@@ -20,6 +20,7 @@ import {
2020
FormMessage,
2121
} from '@/components/ui/form';
2222
import { Textarea } from '@/components/ui/textarea';
23+
import { USER_COURSE_SHORT_DESCRIPTION_PROMPT } from '@/constants/ai';
2324
import { TEXTAREA_MAX_LENGTH } from '@/constants/common';
2425
import { ChatCompletionRole } from '@/constants/open-ai';
2526
import { fetcher } from '@/lib/fetcher';
@@ -78,14 +79,14 @@ export const DescriptionForm = ({ initialData, courseId }: DescriptionFormProps)
7879
Description
7980
<div className="flex items-center gap-x-2">
8081
{isEditing && (
81-
<GenerateDescriptionAi
82+
<GenerateTextResponseAi
8283
callback={setNewDescription}
8384
isSubmitting={isSubmitting}
8485
isValid={isValid}
8586
messages={[
8687
{
8788
role: ChatCompletionRole.USER,
88-
content: `Course short description: "${form.getValues().description}".\nUsing the course description provided above, generate a new one. Maximum output symbols - ${Math.round(TEXTAREA_MAX_LENGTH / 1.4)}`,
89+
content: USER_COURSE_SHORT_DESCRIPTION_PROMPT(form.getValues().description),
8990
},
9091
]}
9192
/>

app/(dashboard)/(routes)/teacher/courses/[courseId]/chapters/[chapterId]/_components/form/chapter-description-form.tsx

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ import Markdown from 'react-markdown';
1111
import ScrollToBottom from 'react-scroll-to-bottom';
1212
import * as z from 'zod';
1313

14-
import { GenerateDescriptionAi } from '@/components/ai/generate-description-ai';
14+
import { GenerateTextResponseAi } from '@/components/ai/generate-text-response-ai';
1515
import { CopyClipboard } from '@/components/common/copy-clipboard';
1616
import { Editor } from '@/components/common/editor';
1717
import { Preview } from '@/components/common/preview';
1818
import { Button } from '@/components/ui/button';
1919
import { Form, FormControl, FormField, FormItem, FormMessage } from '@/components/ui/form';
20+
import { USER_CHAPTER_DESCRIPTION_PROMPT } from '@/constants/ai';
2021
import { ChatCompletionRole } from '@/constants/open-ai';
2122
import { fetcher } from '@/lib/fetcher';
2223
import { cn } from '@/lib/utils';
@@ -73,14 +74,16 @@ export const ChapterDescriptionForm = ({
7374
Description
7475
<div className="flex items-center gap-x-2">
7576
{isEditing && (
76-
<GenerateDescriptionAi
77+
<GenerateTextResponseAi
7778
callback={setNewDescription}
7879
isSubmitting={isSubmitting}
7980
isValid={isValid}
8081
messages={[
8182
{
8283
role: ChatCompletionRole.USER,
83-
content: `Chapter description: "${form.getValues().description.replace(/\n$/, '')}".\nUsing the chapter description provided above, generate a new one. Provide only answer without HTML tags.`,
84+
content: USER_CHAPTER_DESCRIPTION_PROMPT(
85+
form.getValues().description.replace(/\n$/, ''),
86+
),
8487
},
8588
]}
8689
/>

app/(docs)/(routes)/releases/page.tsx

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { format } from 'date-fns';
2+
import { ArrowLeft, Download } from 'lucide-react';
3+
import Link from 'next/link';
4+
5+
import { getGithubReleases } from '@/actions/github/get-releases';
6+
import { MarkdownText } from '@/components/common/markdown-text';
7+
import { Button } from '@/components/ui';
8+
import { TIMESTAMP_TEMPLATE } from '@/constants/common';
9+
10+
type ReleasesPagePageProps = {
11+
searchParams: { pageIndex: string; pageSize: string };
12+
};
13+
14+
const ReleasesPage = async ({ searchParams }: ReleasesPagePageProps) => {
15+
const releases = await getGithubReleases(searchParams);
16+
17+
return (
18+
<div className="p-6 flex flex-col mb-6">
19+
<div className="w-full">
20+
<Link
21+
className="flex items-center text-sm hover:opacity-75 transition duration-300 mb-6"
22+
href={'/'}
23+
>
24+
<ArrowLeft className="h-4 w-4 mr-2" />
25+
Back to courses
26+
</Link>
27+
</div>
28+
<h1 className="text-2xl font-medium mb-12">Releases notes</h1>
29+
{releases.map((release) => (
30+
<div className="mb-8" key={release.name}>
31+
<div className="flex flex-col mb-4">
32+
{release.html_url && (
33+
<Link href={release.html_url} target="_blank">
34+
<h2 className="font-bold text-xl">{release.name}</h2>
35+
</Link>
36+
)}
37+
{release.publishedAt && (
38+
<p className="text-sm text-muted-foreground mb-4">
39+
{format(release.publishedAt, TIMESTAMP_TEMPLATE)}
40+
</p>
41+
)}
42+
{release.zipUrl && (
43+
<Link href={release.zipUrl}>
44+
<Button variant="outline">
45+
<Download className="h-4 w-4 mr-2" />
46+
Download .zip
47+
</Button>
48+
</Link>
49+
)}
50+
</div>
51+
<MarkdownText text={release.body} />
52+
</div>
53+
))}
54+
</div>
55+
);
56+
};
57+
58+
export default ReleasesPage;

app/(docs)/layout.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { getCurrentUser } from '@/actions/auth/get-current-user';
2+
import { getGlobalProgress } from '@/actions/courses/get-global-progress';
3+
import { getUserNotifications } from '@/actions/users/get-user-notifications';
4+
import { NavBar } from '@/components/navbar/navbar';
5+
6+
type DocsLayoutProps = Readonly<{
7+
children: React.ReactNode;
8+
}>;
9+
10+
const DocsLayout = async ({ children }: DocsLayoutProps) => {
11+
const user = await getCurrentUser();
12+
const globalProgress = await getGlobalProgress(user?.userId);
13+
const { notifications: userNotifications } = await getUserNotifications({
14+
userId: user?.userId,
15+
take: 5,
16+
});
17+
18+
return (
19+
<div className="h-full flex flex-col">
20+
<div className="flex-1 h-full">
21+
<div className="h-[80px] inset-y-0 w-full z-[50] fixed">
22+
<NavBar isChat globalProgress={globalProgress} userNotifications={userNotifications} />
23+
</div>
24+
<main className="pt-[80px] h-full">{children}</main>
25+
</div>
26+
</div>
27+
);
28+
};
29+
30+
export default DocsLayout;

components/ai/generate-description-ai.tsx renamed to components/ai/generate-text-response-ai.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,23 @@ import { Dispatch, SetStateAction, useRef, useState } from 'react';
55
import toast from 'react-hot-toast';
66
import { BsStars } from 'react-icons/bs';
77

8+
import { SYSTEM_COURSE_PROMPT } from '@/constants/ai';
89
import { ChatCompletionRole, DEFAULT_MODEL } from '@/constants/open-ai';
910
import { fetcher } from '@/lib/fetcher';
1011

11-
type GenerateDescriptionAiProps = {
12+
type GenerateTextResponseAiProps = {
1213
callback: Dispatch<SetStateAction<string>>;
1314
isSubmitting?: boolean;
1415
isValid?: boolean;
1516
messages: { role: string; content: string }[];
1617
};
1718

18-
export const GenerateDescriptionAi = ({
19+
export const GenerateTextResponseAi = ({
1920
callback,
2021
isSubmitting,
2122
isValid,
2223
messages,
23-
}: GenerateDescriptionAiProps) => {
24+
}: GenerateTextResponseAiProps) => {
2425
const abortControllerRef = useRef<AbortController | null>(null);
2526

2627
const [isImproving, setIsImproving] = useState(false);
@@ -37,7 +38,7 @@ export const GenerateDescriptionAi = ({
3738
messages,
3839
system: {
3940
role: ChatCompletionRole.SYSTEM,
40-
content: 'You are the creator of various courses on a special learning platform.',
41+
content: SYSTEM_COURSE_PROMPT,
4142
},
4243
model: DEFAULT_MODEL,
4344
},

components/common/markdown-text.tsx

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use client';
2+
3+
import Markdown from 'react-markdown';
4+
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
5+
import { atomDark } from 'react-syntax-highlighter/dist/cjs/styles/prism';
6+
import remarkGfm from 'remark-gfm';
7+
8+
type MarkdownTextProps = {
9+
text?: string | null;
10+
};
11+
12+
export const MarkdownText = ({ text }: MarkdownTextProps) => {
13+
return (
14+
<div className="text-sm prose dark:prose-invert prose-a:text-accent-primary prose-a:no-underline hover:prose-a:underline prose-pre:bg-transparent">
15+
<Markdown
16+
remarkPlugins={[remarkGfm]}
17+
components={{
18+
code({ inline, className, children, ...props }: any) {
19+
const match = /language-(\w+)/.exec(className || '');
20+
21+
return !inline && match ? (
22+
<SyntaxHighlighter
23+
{...props}
24+
PreTag="div"
25+
language={match[1]}
26+
style={atomDark}
27+
wrapLongLines
28+
>
29+
{String(children).replace(/\n$/, '')}
30+
</SyntaxHighlighter>
31+
) : (
32+
<code {...props} className={className || ''}>
33+
{children}
34+
</code>
35+
);
36+
},
37+
}}
38+
>
39+
{text}
40+
</Markdown>
41+
</div>
42+
);
43+
};

components/footer/footer.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const Footer = () => {
3232
</div>
3333
</div>
3434
<div className="gap-1 md:gap-2 font-semibold flex flex-col md:flex-row">
35+
<Link href="/releases">Releases notes</Link>
3536
<Link href={TERMS_AND_CONDITIONS_URL} target="_blank">
3637
Terms and Conditions
3738
</Link>

constants/ai.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { TEXTAREA_MAX_LENGTH } from './common';
2+
3+
export const SYSTEM_COURSE_PROMPT =
4+
'You are the creator of various courses on a special learning platform.';
5+
6+
export const USER_COURSE_SHORT_DESCRIPTION_PROMPT = (originalDescription: string) =>
7+
`Course short description: "${originalDescription}".\nUsing the course description provided above, generate a new one in other words. Maximum output symbols - ${Math.round(TEXTAREA_MAX_LENGTH / 1.4)}`;
8+
export const USER_CHAPTER_DESCRIPTION_PROMPT = (originalDescription: string) =>
9+
`Chapter description: "${originalDescription}".\nUsing the chapter description provided above, generate a new one in other words. Provide only answer without HTML tags.`;

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@
9090
"react-syntax-highlighter": "^15.5.0",
9191
"react-tag-autocomplete": "^7.2.0",
9292
"recharts": "^2.12.0",
93+
"remark-gfm": "^4.0.0",
9394
"sharp": "^0.32.6",
9495
"stripe": "^14.16.0",
9596
"tailwind-merge": "^2.2.1",

0 commit comments

Comments
 (0)