From 1b7ac8eec341622b27b2b8a7ec91b8ff73baba99 Mon Sep 17 00:00:00 2001 From: Wellington Neto Date: Mon, 13 Apr 2026 21:20:10 -0300 Subject: [PATCH 1/3] adding next auth and logging into the client app --- .idea/.idea.NotNull/.idea/dataSources.xml | 7 ++ webapp/package-lock.json | 103 ++++++++++++++++++ webapp/package.json | 1 + .../src/app/api/auth/[...nextauth]/route.ts | 2 + webapp/src/auth.ts | 12 ++ webapp/src/components/nav/LoginButton.tsx | 19 ++++ webapp/src/components/nav/TopNav.tsx | 9 +- 7 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 webapp/src/app/api/auth/[...nextauth]/route.ts create mode 100644 webapp/src/auth.ts create mode 100644 webapp/src/components/nav/LoginButton.tsx diff --git a/.idea/.idea.NotNull/.idea/dataSources.xml b/.idea/.idea.NotNull/.idea/dataSources.xml index 2d01e44..5a194dd 100644 --- a/.idea/.idea.NotNull/.idea/dataSources.xml +++ b/.idea/.idea.NotNull/.idea/dataSources.xml @@ -43,5 +43,12 @@ jdbc:postgresql://localhost:55394/?password=db8yXrm8kX0%7E2fchM%2BJ%2BBc&user=postgres $ProjectFileDir$ + + postgresql + true + org.postgresql.Driver + jdbc:postgresql://localhost:56892/?password=db8yXrm8kX0%7E2fchM%2BJ%2BBc&user=postgres + $ProjectFileDir$ + \ No newline at end of file diff --git a/webapp/package-lock.json b/webapp/package-lock.json index 44cd396..827c5c9 100644 --- a/webapp/package-lock.json +++ b/webapp/package-lock.json @@ -14,6 +14,7 @@ "date-fns": "^4.1.0", "framer-motion": "^12.26.2", "next": "16.1.3", + "next-auth": "^5.0.0-beta.30", "next-themes": "^0.4.6", "react": "19.2.3", "react-dom": "19.2.3", @@ -45,6 +46,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@auth/core": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@auth/core/-/core-0.41.0.tgz", + "integrity": "sha512-Wd7mHPQ/8zy6Qj7f4T46vg3aoor8fskJm6g2Zyj064oQ3+p0xNZXAV60ww0hY+MbTesfu29kK14Zk5d5JTazXQ==", + "license": "ISC", + "dependencies": { + "@panva/hkdf": "^1.2.1", + "jose": "^6.0.6", + "oauth4webapi": "^3.3.0", + "preact": "10.24.3", + "preact-render-to-string": "6.5.11" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "nodemailer": "^6.8.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/@babel/code-frame": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", @@ -2847,6 +2877,15 @@ "node": ">=12.4.0" } }, + "node_modules/@panva/hkdf": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@panva/hkdf/-/hkdf-1.2.1.tgz", + "integrity": "sha512-6oclG6Y3PiDFcoyk8srjLfVKyMfVCKJ27JwNPViuXziFpmdz+MZnZN/aKY0JGXgYuO/VghU0jcOAZgWXZ1Dmrw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/@react-aria/breadcrumbs": { "version": "3.5.30", "resolved": "https://registry.npmjs.org/@react-aria/breadcrumbs/-/breadcrumbs-3.5.30.tgz", @@ -7563,6 +7602,15 @@ "jiti": "lib/jiti-cli.mjs" } }, + "node_modules/jose": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.2.tgz", + "integrity": "sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -8180,6 +8228,33 @@ } } }, + "node_modules/next-auth": { + "version": "5.0.0-beta.30", + "resolved": "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.30.tgz", + "integrity": "sha512-+c51gquM3F6nMVmoAusRJ7RIoY0K4Ts9HCCwyy/BRoe4mp3msZpOzYMyb5LAYc1wSo74PMQkGDcaghIO7W6Xjg==", + "license": "ISC", + "dependencies": { + "@auth/core": "0.41.0" + }, + "peerDependencies": { + "@simplewebauthn/browser": "^9.0.1", + "@simplewebauthn/server": "^9.0.2", + "next": "^14.0.0-0 || ^15.0.0 || ^16.0.0", + "nodemailer": "^7.0.7", + "react": "^18.2.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@simplewebauthn/browser": { + "optional": true + }, + "@simplewebauthn/server": { + "optional": true + }, + "nodemailer": { + "optional": true + } + } + }, "node_modules/next-themes": { "version": "0.4.6", "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.6.tgz", @@ -8225,6 +8300,15 @@ "dev": true, "license": "MIT" }, + "node_modules/oauth4webapi": { + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/oauth4webapi/-/oauth4webapi-3.8.5.tgz", + "integrity": "sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -8528,6 +8612,25 @@ "node": ">=4" } }, + "node_modules/preact": { + "version": "10.24.3", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.24.3.tgz", + "integrity": "sha512-Z2dPnBnMUfyQfSQ+GBdsGa16hz35YmLmtTLhM169uW944hYL6xzTYkJjC07j+Wosz733pMWx0fgON3JNw1jJQA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, + "node_modules/preact-render-to-string": { + "version": "6.5.11", + "resolved": "https://registry.npmjs.org/preact-render-to-string/-/preact-render-to-string-6.5.11.tgz", + "integrity": "sha512-ubnauqoGczeGISiOh6RjX0/cdaF8v/oDXIjO85XALCQjwQP+SB4RDXXtvZ6yTYSjG+PC1QRP2AhPgCEsM2EvUw==", + "license": "MIT", + "peerDependencies": { + "preact": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", diff --git a/webapp/package.json b/webapp/package.json index 9c67756..bb88822 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -15,6 +15,7 @@ "date-fns": "^4.1.0", "framer-motion": "^12.26.2", "next": "16.1.3", + "next-auth": "^5.0.0-beta.30", "next-themes": "^0.4.6", "react": "19.2.3", "react-dom": "19.2.3", diff --git a/webapp/src/app/api/auth/[...nextauth]/route.ts b/webapp/src/app/api/auth/[...nextauth]/route.ts new file mode 100644 index 0000000..7038c61 --- /dev/null +++ b/webapp/src/app/api/auth/[...nextauth]/route.ts @@ -0,0 +1,2 @@ +import { handlers } from "@/auth" +export const { GET, POST } = handlers; \ No newline at end of file diff --git a/webapp/src/auth.ts b/webapp/src/auth.ts new file mode 100644 index 0000000..0d6162c --- /dev/null +++ b/webapp/src/auth.ts @@ -0,0 +1,12 @@ +import NextAuth from "next-auth"; +import KeycloakProvider from "next-auth/providers/keycloak"; + +export const { handlers, signIn, signOut, auth } = NextAuth({ + providers: [ + KeycloakProvider({ + clientId: process.env.KEYCLOAK_ID, + clientSecret: process.env.KEYCLOAK_SECRET, + issuer: process.env.KEYCLOAK_ISSUER, + }) + ], +}) diff --git a/webapp/src/components/nav/LoginButton.tsx b/webapp/src/components/nav/LoginButton.tsx new file mode 100644 index 0000000..6ed2750 --- /dev/null +++ b/webapp/src/components/nav/LoginButton.tsx @@ -0,0 +1,19 @@ +'use client'; + +import {Button} from "@heroui/react"; +import {signIn} from "next-auth/react"; + +export default function LoginButton() { + return ( + + ); +} + diff --git a/webapp/src/components/nav/TopNav.tsx b/webapp/src/components/nav/TopNav.tsx index 79747ff..7dbc134 100644 --- a/webapp/src/components/nav/TopNav.tsx +++ b/webapp/src/components/nav/TopNav.tsx @@ -5,6 +5,7 @@ import { CommandLineIcon, MagnifyingGlassIcon, BellIcon, UserCircleIcon } from " import { Button, Input, Badge } from "@heroui/react"; import ThemeToggle from "@/components/nav/ThemeToggle"; import SearchInput from "@/components/nav/SearchInput"; +import LoginButton from "./LoginButton"; export default function TopNav() { return ( @@ -53,13 +54,7 @@ export default function TopNav() {
- + + ); +} \ No newline at end of file diff --git a/webapp/src/components/nav/TopNav.tsx b/webapp/src/components/nav/TopNav.tsx index 7dbc134..c1ac271 100644 --- a/webapp/src/components/nav/TopNav.tsx +++ b/webapp/src/components/nav/TopNav.tsx @@ -1,13 +1,16 @@ -"use client"; - -import Link from "next/link"; +import Link from "next/link"; import { CommandLineIcon, MagnifyingGlassIcon, BellIcon, UserCircleIcon } from "@heroicons/react/24/outline"; import { Button, Input, Badge } from "@heroui/react"; import ThemeToggle from "@/components/nav/ThemeToggle"; import SearchInput from "@/components/nav/SearchInput"; import LoginButton from "./LoginButton"; +import {getCurrentUser} from "@/lib/actions/auth-actions"; +import SignUpButton from "@/components/nav/SignUpButton"; -export default function TopNav() { +export default async function TopNav() { + + const user = await getCurrentUser(); + return (
@@ -54,13 +57,14 @@ export default function TopNav() {
- - + {user ? ( +
{user.name}
+ ) : ( + <> + + + + )}
diff --git a/webapp/src/lib/actions/auth-actions.ts b/webapp/src/lib/actions/auth-actions.ts index b151ab1..5e7c507 100644 --- a/webapp/src/lib/actions/auth-actions.ts +++ b/webapp/src/lib/actions/auth-actions.ts @@ -1,7 +1,22 @@ 'use server' import { fetchClient } from "../fetchClient" +import {auth} from "@/auth"; export async function testAuth(){ return fetchClient(`/test/auth`, 'GET') +} + +export async function getCurrentUser(){ + try { + const session = await auth(); + + if(!session) return null; + + return session.user; + + }catch(err){ + console.error(err) + return null; + } } \ No newline at end of file From d8aef85c1d13b73eb02efb6472d081802cc1d6a0 Mon Sep 17 00:00:00 2001 From: Wellington Neto Date: Mon, 13 Apr 2026 21:52:51 -0300 Subject: [PATCH 3/3] refact style --- webapp/src/app/questions/QuestionCard.tsx | 107 +++++++++----- webapp/src/app/questions/QuestionsHeader.tsx | 91 +++++++----- .../src/app/questions/[id]/AnswerContent.tsx | 34 +++-- .../src/app/questions/[id]/AnswerFooter.tsx | 42 +++--- .../src/app/questions/[id]/AnswersHeader.tsx | 41 +++--- .../app/questions/[id]/QuestionContent.tsx | 66 +++++++-- .../questions/[id]/QuestionDetailedHeader.tsx | 54 +++---- .../src/app/questions/[id]/QuestionFooter.tsx | 43 +++--- .../src/app/questions/[id]/VotingButtons.tsx | 54 ++++--- webapp/src/components/nav/LoginButton.tsx | 19 ++- webapp/src/components/nav/SearchInput.tsx | 92 ++++++++---- webapp/src/components/nav/SignUpButton.tsx | 12 +- webapp/src/components/nav/TopNav.tsx | 139 +++++++++++------- 13 files changed, 500 insertions(+), 294 deletions(-) diff --git a/webapp/src/app/questions/QuestionCard.tsx b/webapp/src/app/questions/QuestionCard.tsx index 9abd16d..8677ab3 100644 --- a/webapp/src/app/questions/QuestionCard.tsx +++ b/webapp/src/app/questions/QuestionCard.tsx @@ -1,86 +1,117 @@ 'use client' -import {Question} from "@/lib/types"; +import { Question } from "@/lib/types"; import Link from "next/link"; -import {Chip} from "@heroui/chip"; -import {Avatar} from "@heroui/avatar"; +import { Chip } from "@heroui/chip"; +import { Avatar } from "@heroui/avatar"; import clsx from "clsx"; -import {CheckIcon} from "@heroicons/react/24/outline"; +import { CheckIcon } from "@heroicons/react/24/outline"; import { timeAgo } from "@/lib/util"; type Props = { question: Question; } -export default function QuestionCard({question}: Props) { +export default function QuestionCard({ question }: Props) { return ( -
-
- {question.votes} - {question.votes === 1 ? 'vote' : 'votes'} +
+ {/* Stats column */} +
+ + {/* Votes */} +
+ + {question.votes} + + + {question.votes === 1 ? 'vote' : 'votes'} +
-
0 && !question.hasAcceptedAnswer, - 'bg-success-500 text-white shadow-md': question.hasAcceptedAnswer, - 'bg-default-100': question.answerCount === 0 - })} - > -
+ {/* Answers */} +
0 && !question.hasAcceptedAnswer, + 'bg-default-100 dark:bg-white/5 text-default-500': question.answerCount === 0, + } + )}> +
{question.hasAcceptedAnswer && ( - + )} - {question.answerCount} + {question.answerCount}
- {question.answerCount === 1 ? 'answer' : 'answers'} + + {question.answerCount === 1 ? 'answer' : 'answers'} +
-
- {question.viewCount} - {question.viewCount === 1 ? 'view' : 'views'} + {/* Views */} +
+ + {question.viewCount} + + + {question.viewCount === 1 ? 'view' : 'views'} +
- -
+ + {/* Main content */} +
{question.title}
-
- {/* Tags */} -
+ {/* Footer: tags + author */} +
+
{question.tagSlugs.map(tag => ( {tag} ))}
-
+
{question.askerDisplayName} diff --git a/webapp/src/app/questions/QuestionsHeader.tsx b/webapp/src/app/questions/QuestionsHeader.tsx index e8d34ce..edd8e8c 100644 --- a/webapp/src/app/questions/QuestionsHeader.tsx +++ b/webapp/src/app/questions/QuestionsHeader.tsx @@ -1,9 +1,9 @@ 'use client'; -import {Button} from "@heroui/button"; +import { Button } from "@heroui/button"; import Link from "next/link"; -import {Tab, Tabs} from "@heroui/tabs"; -import {useTagStore} from "@/lib/UseTagStore"; +import { Tab, Tabs } from "@heroui/tabs"; +import { useTagStore } from "@/lib/UseTagStore"; type Props = { tag?: string; @@ -11,63 +11,80 @@ type Props = { } export default function QuestionsHeader({ tag, total }: Props) { - const selectedTag = useTagStore((state) => state.getTagBySlug(tag ?? '')); - - + const tabs = [ - {key: 'newest', label: 'Newest'}, - {key: 'active', label: 'Active'}, - {key: 'unanswered', label: 'Unanswered'}, + { key: 'newest', label: 'Newest' }, + { key: 'active', label: 'Active' }, + { key: 'unanswered', label: 'Unanswered' }, ]; return ( -
-
-
-
-

- {tag ? ( - - Questions tagged [{tag}] - - ) : ( - 'All Questions' - )} -

-

- {selectedTag?.description} +

+ + {/* Top bar */} +
+
+

+ {tag ? ( + <> + Questions tagged{' '} + + {tag} + + + ) : ( + 'All Questions' + )} +

+ + {selectedTag?.description && ( +

+ {selectedTag.description}

-
-

+ )} + +

{total.toLocaleString()} {total === 1 ? 'question' : 'questions'}

{/* Filter tabs */} -
+
{tabs.map(tab => ( - + ))}
diff --git a/webapp/src/app/questions/[id]/AnswerContent.tsx b/webapp/src/app/questions/[id]/AnswerContent.tsx index bf82d08..eed86a5 100644 --- a/webapp/src/app/questions/[id]/AnswerContent.tsx +++ b/webapp/src/app/questions/[id]/AnswerContent.tsx @@ -1,19 +1,33 @@ -import {Answer} from "@/lib/types"; +import { Answer } from "@/lib/types"; import VotingButtons from "@/app/questions/[id]/VotingButtons"; import AnswerFooter from "@/app/questions/[id]/AnswerFooter"; -type Props = { - answer: Answer; -} - -export default function AnswerContent({answer}: {answer: Answer}) { +export default function AnswerContent({ answer }: { answer: Answer }) { return ( -
+
-
+ +
diff --git a/webapp/src/app/questions/[id]/AnswerFooter.tsx b/webapp/src/app/questions/[id]/AnswerFooter.tsx index 148006c..d63f809 100644 --- a/webapp/src/app/questions/[id]/AnswerFooter.tsx +++ b/webapp/src/app/questions/[id]/AnswerFooter.tsx @@ -1,26 +1,30 @@ -import {Answer} from "@/lib/types"; +import { Answer } from "@/lib/types"; import { timeAgo } from "@/lib/util"; -import {Avatar} from "@heroui/avatar"; +import { Avatar } from "@heroui/avatar"; import Link from "next/link"; -export default function AnswerFooter({answer}: {answer: Answer}) { +export default function AnswerFooter({ answer }: { answer: Answer }) { return ( -
-
-
- answered {timeAgo(answer.createdAt)} - - - {answer.userDisplayName} - -
+
+
+ + answered {timeAgo(answer.createdAt)} + + + + {answer.userDisplayName} +
); diff --git a/webapp/src/app/questions/[id]/AnswersHeader.tsx b/webapp/src/app/questions/[id]/AnswersHeader.tsx index bf469cf..b76737b 100644 --- a/webapp/src/app/questions/[id]/AnswersHeader.tsx +++ b/webapp/src/app/questions/[id]/AnswersHeader.tsx @@ -1,25 +1,30 @@ 'use client' -import {Select, SelectItem} from "@heroui/select"; -type Props = { - answerCount: number; -} +import { Select, SelectItem } from "@heroui/select"; -export default function AnswersHeader({ answerCount }: Props) { +export default function AnswersHeader({ answerCount }: { answerCount: number }) { return ( -
-
+
+

{answerCount} {answerCount === 1 ? 'Answer' : 'Answers'} -

-
- -
+ + +
); -} - +} \ No newline at end of file diff --git a/webapp/src/app/questions/[id]/QuestionContent.tsx b/webapp/src/app/questions/[id]/QuestionContent.tsx index d57f049..1b9e81e 100644 --- a/webapp/src/app/questions/[id]/QuestionContent.tsx +++ b/webapp/src/app/questions/[id]/QuestionContent.tsx @@ -8,35 +8,71 @@ import { timeAgo } from "@/lib/util"; export default function QuestionContent({ question }: { question: Question }) { return ( -
-
+
+
-
-
-
+ {/* Content box */} +
+
- -
-
- - asked {timeAgo(question.createdAt)} - + + {/* Author card */} +
+
+
+ asked + + {timeAgo(question.createdAt)} + +
+ + {question.askerDisplayName} diff --git a/webapp/src/app/questions/[id]/QuestionDetailedHeader.tsx b/webapp/src/app/questions/[id]/QuestionDetailedHeader.tsx index 77abb81..df4077b 100644 --- a/webapp/src/app/questions/[id]/QuestionDetailedHeader.tsx +++ b/webapp/src/app/questions/[id]/QuestionDetailedHeader.tsx @@ -1,46 +1,50 @@ -import {Question} from "@/lib/types"; -import {Button} from "@heroui/button"; -import {LinkComponent} from "@/components/nav/LinkComponent"; -import {fuzzyTimeAgo} from "@/lib/util"; +import { Question } from "@/lib/types"; +import { Button } from "@heroui/button"; +import { LinkComponent } from "@/components/nav/LinkComponent"; +import { fuzzyTimeAgo } from "@/lib/util"; -type Props = { - question: Question; -} - -export default function QuestionDetailedHeader({question}: Props) { +export default function QuestionDetailedHeader({ question }: { question: Question }) { return ( -
-
-

+
+
+

{question.title}

-
-
+
+
Asked - {fuzzyTimeAgo(question.createdAt)} + + {fuzzyTimeAgo(question.createdAt)} +
- {question.updatedAt && ( -
+
Modified - {fuzzyTimeAgo(question.updatedAt)} + + {fuzzyTimeAgo(question.updatedAt)} +
)} - -
+
Viewed - {question.viewCount + 1} times + + {question.viewCount + 1} times +
diff --git a/webapp/src/app/questions/[id]/QuestionFooter.tsx b/webapp/src/app/questions/[id]/QuestionFooter.tsx index 26be2cd..5f8fcf5 100644 --- a/webapp/src/app/questions/[id]/QuestionFooter.tsx +++ b/webapp/src/app/questions/[id]/QuestionFooter.tsx @@ -1,28 +1,27 @@ -import {Question} from "@/lib/types"; -import {Chip} from "@heroui/chip"; -import {Avatar} from "@heroui/avatar"; +import { Question } from "@/lib/types"; +import { Chip } from "@heroui/chip"; +import { Avatar } from "@heroui/avatar"; import { LinkComponent } from "@/components/nav/LinkComponent"; - -type Props = { - question: Question; -} -export default function QuestionFooter({question}: Props) { +export default function QuestionFooter({ question }: { question: Question }) { return ( -
-
-
- {question.tagSlugs.map((tag: string) => ( - - {tag} - - ))} -
+
+ {/* Tags */} +
+ {question.tagSlugs.map((tag: string) => ( + + {tag} + + ))}
); diff --git a/webapp/src/app/questions/[id]/VotingButtons.tsx b/webapp/src/app/questions/[id]/VotingButtons.tsx index 2ef0f38..00b2152 100644 --- a/webapp/src/app/questions/[id]/VotingButtons.tsx +++ b/webapp/src/app/questions/[id]/VotingButtons.tsx @@ -1,43 +1,49 @@ -import {Button} from "@heroui/button"; -import {ArrowDownCircleIcon, ArrowUpCircleIcon} from "@heroicons/react/24/outline"; -import {CheckBadgeIcon} from "@heroicons/react/24/solid"; +import { Button } from "@heroui/button"; +import { ArrowDownCircleIcon, ArrowUpCircleIcon } from "@heroicons/react/24/outline"; +import { CheckBadgeIcon } from "@heroicons/react/24/solid"; type Props = { accepted?: boolean; } -export default function VotingButtons({accepted}: Props){ +export default function VotingButtons({ accepted }: Props) { return ( -
+
-
- 0 + +
+ 0
+ + {accepted && ( -
- +
+
+ +
)}
diff --git a/webapp/src/components/nav/LoginButton.tsx b/webapp/src/components/nav/LoginButton.tsx index 6ed2750..cc21e16 100644 --- a/webapp/src/components/nav/LoginButton.tsx +++ b/webapp/src/components/nav/LoginButton.tsx @@ -1,19 +1,22 @@ 'use client'; -import {Button} from "@heroui/react"; -import {signIn} from "next-auth/react"; +import { Button } from "@heroui/react"; +import { signIn } from "next-auth/react"; export default function LoginButton() { return ( ); -} - +} \ No newline at end of file diff --git a/webapp/src/components/nav/SearchInput.tsx b/webapp/src/components/nav/SearchInput.tsx index 916ca22..b77b3dd 100644 --- a/webapp/src/components/nav/SearchInput.tsx +++ b/webapp/src/components/nav/SearchInput.tsx @@ -19,10 +19,8 @@ export default function SearchInput() { if (timeoutRef.current) clearTimeout(timeoutRef.current); if (!query) { - timeoutRef.current = setTimeout(() => { - setResults(null); - setShowDropdown(false); - }, 0); + setResults(null); + setShowDropdown(false); return; } @@ -60,35 +58,65 @@ export default function SearchInput() { - : + ?
+ : } type="search" placeholder="Search questions..." value={query} onChange={(e) => setQuery(e.target.value)} onFocus={() => results && setShowDropdown(true)} + classNames={{ + inputWrapper: [ + 'border border-default-200 dark:border-white/10', + 'bg-gray-50 dark:bg-white/5', + 'hover:border-purple-300 dark:hover:border-purple-700', + 'group-data-[focus=true]:border-purple-500 dark:group-data-[focus=true]:border-purple-400', + 'shadow-none', + 'transition-colors duration-150', + ].join(' '), + }} /> {showDropdown && results && ( -
+
{results.length === 0 ? ( -
- No results for{' '} - - “{query}” - +
+ +

+ No results for{' '} + + “{query}” + +

) : ( <> -
- {results.length} result{results.length !== 1 ? 's' : ''} found + {/* Header do dropdown */} +
+ + {results.length} result{results.length !== 1 ? 's' : ''} found + + + ↵ to select +
+ {(question) => ( 0 - ? 'border-success text-success bg-success/10' - : 'border-default-300 text-default-400' - }`} - > - {question.answerCount} - answers +
0 + ? 'border-emerald-300 dark:border-emerald-700 text-emerald-600 dark:text-emerald-400 bg-emerald-50 dark:bg-emerald-950/40' + : 'border-default-200 dark:border-white/10 text-default-400 bg-default-50 dark:bg-white/5' + } + `}> + + {question.answerCount} + + + ans +
} >
- {question.title} + + {question.title} + {question.content} diff --git a/webapp/src/components/nav/SignUpButton.tsx b/webapp/src/components/nav/SignUpButton.tsx index a07e407..afe158b 100644 --- a/webapp/src/components/nav/SignUpButton.tsx +++ b/webapp/src/components/nav/SignUpButton.tsx @@ -1,12 +1,20 @@ -"use client"; +// src/components/nav/SignUpButton.tsx +'use client'; import { Button } from "@heroui/react"; export default function SignUpButton() { return ( diff --git a/webapp/src/components/nav/TopNav.tsx b/webapp/src/components/nav/TopNav.tsx index c1ac271..cd34129 100644 --- a/webapp/src/components/nav/TopNav.tsx +++ b/webapp/src/components/nav/TopNav.tsx @@ -1,72 +1,111 @@ import Link from "next/link"; -import { CommandLineIcon, MagnifyingGlassIcon, BellIcon, UserCircleIcon } from "@heroicons/react/24/outline"; -import { Button, Input, Badge } from "@heroui/react"; +import { CommandLineIcon } from "@heroicons/react/24/outline"; import ThemeToggle from "@/components/nav/ThemeToggle"; import SearchInput from "@/components/nav/SearchInput"; import LoginButton from "./LoginButton"; -import {getCurrentUser} from "@/lib/actions/auth-actions"; import SignUpButton from "@/components/nav/SignUpButton"; +import { getCurrentUser } from "@/lib/actions/auth-actions"; + +const navLinks = [ + { href: '/questions', label: 'Questions' }, + { href: '/tags', label: 'Tags' }, + { href: '/users', label: 'Users' }, +]; export default async function TopNav() { - const user = await getCurrentUser(); - + return ( -
-
-
- -
- +
+
+ + {/* ── Left: logo + nav + search ── */} +
+ + {/* Logo */} + +
+
-

+ @NotNull -

+ -
- - -
- - - -
- {user ? ( -
{user.name}
- ) : ( - <> - - - - )} + + {/* Search */} +
+
+ + {/* ── Right: theme toggle + auth ── */} +
+ + + + {user ? ( + /* User avatar/name pill */ +
+
+ {user.name?.charAt(0).toUpperCase()} +
+ + {user.name} + +
+ ) : ( +
+ + +
+ )} +
);