mirror of
https://github.com/linsa-io/linsa.git
synced 2026-04-20 07:21:34 +02:00
fix: link (#115)
* start * . * seeding connections * . * wip * wip: learning state * wip: notes section * wip: many * topics * chore: update schema * update package * update sidebar * update page section * feat: profile * fix: remove z index * fix: wrong type * add avatar * add avatar * wip * . * store page section key * remove atom page section * fix rerender * fix rerender * fix rerender * fix rerender * fix link * search light/dark mode * bubble menu ui * . * fix: remove unecessary code * chore: mark as old for old schema * chore: adapt new schema * fix: add topic schema but null for now * fix: add icon on personal link * fix: list item * fix: set url fetched when editing * fix: remove image * feat: add icon to link * feat: custom url zod validation * fix: metadata test * chore: update utils * fix: link * fix: url fetcher * . * . * fix: add link, section * chore: seeder * . * . * . * . * fix: change checkbox to learning state * fix: click outside editing form * feat: constant * chore: move to master folder * chore: adapt new schema * chore: cli for new schema * fix: new schema for dev seed * fix: seeding * update package * chore: forcegraph seed * bottombar * if isEdit delete icon * showCreate X button * . * options * chore: implement topic from public global group * chore: update learning state * fix: change implementation for outside click * chore: implement new form param * chore: update env example * feat: link form refs exception * new page button layout, link topic search fixed * chore: enable topic * chore: update seed * profile * chore: move framer motion package from root to web and add nuqs * chore: add LearningStateValue * chore: implement active state * profile * chore: use fancy switch and update const * feat: filter implementation * dropdown menu * . * sidebar topics * topic selected color * feat: topic detail * fix: collapsible page * pages - sorted by, layout, visible mode * . * . * . * topic status sidebar * topic button and count * fix: topic * page delete/topic buttons * search ui * selected topic for page * selected topic status sidebar * removed footer * update package * . --------- Co-authored-by: Nikita <github@nikiv.dev> Co-authored-by: marshennikovaolga <marshennikova@gmail.com> Co-authored-by: Kisuyo <ig.intr3st@gmail.com>
This commit is contained in:
5
web/app/(pages)/(topics)/[name]/page.tsx
Normal file
5
web/app/(pages)/(topics)/[name]/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import { TopicDetailRoute } from "@/components/routes/topics/detail/TopicDetailRoute"
|
||||
|
||||
export default function DetailTopicPage({ params }: { params: { name: string } }) {
|
||||
return <TopicDetailRoute topicName={params.name} />
|
||||
}
|
||||
5
web/app/(pages)/edit-profile/page.tsx
Normal file
5
web/app/(pages)/edit-profile/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import EditProfileRoute from "@/components/routes/EditProfileRoute"
|
||||
|
||||
export default function EditProfilePage() {
|
||||
return <EditProfileRoute />
|
||||
}
|
||||
@@ -1,15 +1,22 @@
|
||||
import { Sidebar } from "@/components/custom/sidebar/sidebar"
|
||||
import PublicHomeRoute from "@/components/routes/PublicHomeRoute"
|
||||
|
||||
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex h-full min-h-full w-full flex-row items-stretch overflow-hidden">
|
||||
<Sidebar />
|
||||
// TODO: get it from jazz/clerk
|
||||
const loggedIn = true
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<main className="bg-card relative flex flex-auto flex-col place-items-stretch overflow-auto lg:my-2 lg:mr-2 lg:rounded-md lg:border">
|
||||
{children}
|
||||
</main>
|
||||
if (loggedIn) {
|
||||
return (
|
||||
<div className="flex h-full min-h-full w-full flex-row items-stretch overflow-hidden">
|
||||
<Sidebar />
|
||||
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<main className="relative flex flex-auto flex-col place-items-stretch overflow-auto lg:my-2 lg:mr-2 lg:rounded-md lg:border">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
)
|
||||
}
|
||||
return <PublicHomeRoute />
|
||||
}
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
import { LinkWrapper } from "@/components/routes/link/wrapper"
|
||||
|
||||
export default function LinkPage() {
|
||||
return <LinkWrapper />
|
||||
}
|
||||
5
web/app/(pages)/page.tsx
Normal file
5
web/app/(pages)/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
||||
import AuthHomeRoute from "@/components/routes/AuthHomeRoute"
|
||||
|
||||
export default function HomePage() {
|
||||
return <AuthHomeRoute />
|
||||
}
|
||||
@@ -1,14 +1,137 @@
|
||||
"use client"
|
||||
|
||||
import { useAccount } from "@/lib/providers/jazz-provider"
|
||||
import { useParams, useRouter } from "next/navigation"
|
||||
import Link from "next/link"
|
||||
import { LaIcon } from "@/components/custom/la-icon"
|
||||
import { Icon } from "@/components/la-editor/components/ui/icon"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
export const ProfileWrapper = () => {
|
||||
const account = useAccount()
|
||||
interface ProfileStatsProps {
|
||||
number: number
|
||||
label: string
|
||||
}
|
||||
|
||||
interface ProfileLinksProps {
|
||||
linklabel?: string
|
||||
link?: string
|
||||
topic?: string
|
||||
}
|
||||
|
||||
interface ProfilePagesProps {
|
||||
topic?: string
|
||||
}
|
||||
|
||||
const ProfileStats: React.FC<ProfileStatsProps> = ({ number, label }) => {
|
||||
return (
|
||||
<div>
|
||||
<h2>{account.me.profile?.name}</h2>
|
||||
<p>Profile Page</p>
|
||||
<div className="text-center font-semibold text-black/60 dark:text-white">
|
||||
<p className="text-4xl">{number}</p>
|
||||
<p className="text-[#878787]">{label}</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ProfileLinks: React.FC<ProfileLinksProps> = ({ linklabel, link, topic }) => {
|
||||
return (
|
||||
<div className="flex flex-row items-center justify-between bg-[#121212] p-3 text-black dark:text-white">
|
||||
<div className="flex flex-row items-center space-x-3">
|
||||
<p className="text-base text-opacity-90">{linklabel || "Untitled"}</p>
|
||||
<div className="flex cursor-pointer flex-row items-center gap-1">
|
||||
<Icon name="Link" />
|
||||
<p className="text-sm text-opacity-10">{link || "#"}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text0opacity-50 bg-[#1a1a1a] p-2">{topic || "Uncategorized"}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const ProfilePages: React.FC<ProfilePagesProps> = ({ topic }) => {
|
||||
return (
|
||||
<div className="flex flex-row items-center justify-between rounded-lg bg-[#121212] p-3 text-black dark:text-white">
|
||||
<div className="rounded-lg bg-[#1a1a1a] p-2 text-opacity-50">{topic || "Uncategorized"}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export const ProfileWrapper = () => {
|
||||
const account = useAccount()
|
||||
const params = useParams()
|
||||
const username = params.username as string
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const clickEdit = () => router.push("/edit-profile")
|
||||
|
||||
if (!account.me || !account.me.profile) {
|
||||
return (
|
||||
<div className="flex h-screen flex-col py-3 text-black dark:text-white">
|
||||
<div className="flex flex-1 flex-col rounded-3xl border border-neutral-800">
|
||||
<p className="my-10 h-[74px] border-b border-neutral-900 text-center text-2xl font-semibold">
|
||||
Oops! This account doesn't exist.
|
||||
</p>
|
||||
<p className="mb-5 text-center text-lg font-semibold">Try searching for another.</p>
|
||||
<p className="mb-5 text-center text-lg font-semibold">
|
||||
The link you followed may be broken, or the page may have been removed. Go back to
|
||||
<Link href="/">
|
||||
<span className="">homepage</span>
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-1 flex-col text-black dark:text-white">
|
||||
<div className="flex items-center justify-between p-[20px]">
|
||||
<p className="text-2xl font-semibold">Profile</p>
|
||||
<Button
|
||||
onClick={clickEdit}
|
||||
className="shadow-outer ml-auto flex h-[34px] cursor-pointer flex-row space-x-2 rounded-lg bg-white px-3 text-black shadow-[1px_1px_1px_1px_rgba(0,0,0,0.3)] hover:bg-black/10 dark:bg-[#222222] dark:text-white dark:hover:opacity-60"
|
||||
>
|
||||
<LaIcon name="UserCog" className="cursor-pointer text-neutral-200" />
|
||||
<span>Edit Profile</span>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-2xl font-semibold">{username}</p>
|
||||
<div className="flex flex-col items-center border-b border-neutral-900 bg-inherit pb-5">
|
||||
<div className="flex w-full max-w-2xl align-top">
|
||||
<div className="mr-3 h-[130px] w-[130px] rounded-md bg-[#222222]" />
|
||||
<div className="ml-6 flex-1">
|
||||
<p className="mb-3 text-[25px] font-semibold">{account.me.profile.name}</p>
|
||||
<div className="mb-1 flex flex-row items-center font-light text-[24]">
|
||||
@<p className="pl-1">{account.me.root?.username}</p>
|
||||
</div>
|
||||
<a href={account.me.root?.website || "#"} className="mb-1 flex flex-row items-center text-sm font-light">
|
||||
<Icon name="Link" />
|
||||
<p className="pl-1">{account.me.root?.website}</p>
|
||||
</a>
|
||||
</div>
|
||||
<button className="shadow-outer ml-auto flex h-[34px] cursor-pointer flex-row items-center justify-center space-x-2 rounded-lg bg-white px-3 text-center font-medium text-black shadow-[1px_1px_1px_1px_rgba(0,0,0,0.3)] hover:bg-black/10 dark:bg-[#222222] dark:text-white dark:hover:opacity-60">
|
||||
Follow
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-10 flex justify-center">
|
||||
<div className="flex flex-row gap-20">
|
||||
<ProfileStats number={account.me.root?.topicsLearning?.length || 0} label="Learning" />
|
||||
<ProfileStats number={account.me.root?.topicsWantToLearn?.length || 0} label="To Learn" />
|
||||
<ProfileStats number={account.me.root?.topicsLearned?.length || 0} label="Learned" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* <div className="mx-auto mt-10 w-[50%] justify-center space-y-1">
|
||||
<p className="pb-3 pl-2 text-base font-light text-white/50">Public Pages</p>
|
||||
{account.me.root?.personalPages?.map((page, index) => <ProfileLinks topic={page.topic?.name} />)}
|
||||
</div>
|
||||
<div className="mx-auto mt-10 w-[50%] justify-center space-y-1">
|
||||
<p className="pb-3 pl-2 text-base font-light text-white/50">Public Links</p>
|
||||
{account.me.root?.personalLinks?.map((link, index) => (
|
||||
<ProfileLinks key={index} linklabel={link.title} link={link.url} topic={link.topic?.name} />
|
||||
))}
|
||||
</div> */}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { Sidebar } from "@/components/custom/sidebar/sidebar"
|
||||
|
||||
export default function TopicsLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="flex h-full min-h-full w-full flex-row items-stretch overflow-hidden">
|
||||
<Sidebar />
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<main className="bg-card relative flex flex-auto flex-col place-items-stretch overflow-auto rounded-md border lg:my-2 lg:mr-2">
|
||||
{children}
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import GlobalTopic from "@/components/routes/globalTopic/globalTopic"
|
||||
|
||||
export default function GlobalTopicPage({ params }: { params: { topic: string } }) {
|
||||
return <GlobalTopic topic={params.topic} />
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
*/
|
||||
import { NextRequest } from "next/server"
|
||||
import axios from "axios"
|
||||
import { GET } from "./route"
|
||||
import { DEFAULT_VALUES, GET } from "./route"
|
||||
|
||||
jest.mock("axios")
|
||||
const mockedAxios = axios as jest.Mocked<typeof axios>
|
||||
@@ -19,7 +19,7 @@ describe("Metadata Fetcher", () => {
|
||||
<head>
|
||||
<title>Test Title</title>
|
||||
<meta name="description" content="Test Description">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" href="/icon.ico">
|
||||
</head>
|
||||
</html>
|
||||
`
|
||||
@@ -37,7 +37,7 @@ describe("Metadata Fetcher", () => {
|
||||
expect(data).toEqual({
|
||||
title: "Test Title",
|
||||
description: "Test Description",
|
||||
favicon: "https://example.com/favicon.ico",
|
||||
icon: "https://example.com/icon.ico",
|
||||
url: "https://example.com"
|
||||
})
|
||||
})
|
||||
@@ -66,9 +66,9 @@ describe("Metadata Fetcher", () => {
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toEqual({
|
||||
title: "No title available",
|
||||
description: "No description available",
|
||||
favicon: null,
|
||||
title: DEFAULT_VALUES.TITLE,
|
||||
description: DEFAULT_VALUES.DESCRIPTION,
|
||||
icon: null,
|
||||
url: "https://example.com"
|
||||
})
|
||||
})
|
||||
@@ -92,9 +92,9 @@ describe("Metadata Fetcher", () => {
|
||||
|
||||
expect(response.status).toBe(200)
|
||||
expect(data).toEqual({
|
||||
title: "No title available",
|
||||
description: "No description available",
|
||||
favicon: null,
|
||||
title: DEFAULT_VALUES.TITLE,
|
||||
description: DEFAULT_VALUES.DESCRIPTION,
|
||||
icon: null,
|
||||
url: "https://example.com"
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,29 +1,39 @@
|
||||
import { NextRequest, NextResponse } from "next/server"
|
||||
import axios from "axios"
|
||||
import * as cheerio from "cheerio"
|
||||
import { ensureUrlProtocol } from "@/lib/utils"
|
||||
import { urlSchema } from "@/lib/utils/schema"
|
||||
|
||||
interface Metadata {
|
||||
title: string
|
||||
description: string
|
||||
favicon: string | null
|
||||
icon: string | null
|
||||
url: string
|
||||
}
|
||||
|
||||
const DEFAULT_VALUES = {
|
||||
TITLE: "No title available",
|
||||
DESCRIPTION: "No description available",
|
||||
IMAGE: null,
|
||||
export const DEFAULT_VALUES = {
|
||||
TITLE: "",
|
||||
DESCRIPTION: "",
|
||||
FAVICON: null
|
||||
}
|
||||
|
||||
export async function GET(request: NextRequest) {
|
||||
const { searchParams } = new URL(request.url)
|
||||
const url = searchParams.get("url")
|
||||
let url = searchParams.get("url")
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 1000))
|
||||
|
||||
if (!url) {
|
||||
return NextResponse.json({ error: "URL is required" }, { status: 400 })
|
||||
}
|
||||
|
||||
const result = urlSchema.safeParse(url)
|
||||
if (!result.success) {
|
||||
throw new Error(result.error.issues.map(issue => issue.message).join(", "))
|
||||
}
|
||||
|
||||
url = ensureUrlProtocol(url)
|
||||
|
||||
try {
|
||||
const { data } = await axios.get(url, {
|
||||
timeout: 5000,
|
||||
@@ -41,13 +51,12 @@ export async function GET(request: NextRequest) {
|
||||
$('meta[name="description"]').attr("content") ||
|
||||
$('meta[property="og:description"]').attr("content") ||
|
||||
DEFAULT_VALUES.DESCRIPTION,
|
||||
favicon:
|
||||
$('link[rel="icon"]').attr("href") || $('link[rel="shortcut icon"]').attr("href") || DEFAULT_VALUES.FAVICON,
|
||||
icon: $('link[rel="icon"]').attr("href") || $('link[rel="shortcut icon"]').attr("href") || DEFAULT_VALUES.FAVICON,
|
||||
url: url
|
||||
}
|
||||
|
||||
if (metadata.favicon && !metadata.favicon.startsWith("http")) {
|
||||
metadata.favicon = new URL(metadata.favicon, url).toString()
|
||||
if (metadata.icon && !metadata.icon.startsWith("http")) {
|
||||
metadata.icon = new URL(metadata.icon, url).toString()
|
||||
}
|
||||
|
||||
return NextResponse.json(metadata)
|
||||
@@ -55,7 +64,7 @@ export async function GET(request: NextRequest) {
|
||||
const defaultMetadata: Metadata = {
|
||||
title: DEFAULT_VALUES.TITLE,
|
||||
description: DEFAULT_VALUES.DESCRIPTION,
|
||||
favicon: DEFAULT_VALUES.FAVICON,
|
||||
icon: DEFAULT_VALUES.FAVICON,
|
||||
url: url
|
||||
}
|
||||
return NextResponse.json(defaultMetadata)
|
||||
|
||||
@@ -1,26 +1,6 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap");
|
||||
|
||||
body {
|
||||
font-family: "Inter", sans-serif;
|
||||
}
|
||||
|
||||
@layer base {
|
||||
body {
|
||||
@apply bg-background text-foreground;
|
||||
font-feature-settings:
|
||||
"rlig" 1,
|
||||
"calt" 1;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
overflow-x: hidden;
|
||||
min-height: 100vh;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
:root {
|
||||
@@ -42,17 +22,19 @@ body {
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 10% 3.9%;
|
||||
--result: 240 5.9% 96%;
|
||||
--ring: 240 5.9% 10%;
|
||||
--radius: 0.5rem;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--boxShadow: rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--background: 240 10% 4.5%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
@@ -69,13 +51,15 @@ body {
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--input: 220 9% 10%;
|
||||
--result: 0 0% 7%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
--boxShadow: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Button } from "@/components/ui/button"
|
||||
import Link from "next/link"
|
||||
|
||||
export default function HomePage() {
|
||||
return (
|
||||
<div className="flex min-h-full items-center justify-center">
|
||||
<Link href="/links">
|
||||
<Button>Go to main page</Button>
|
||||
</Link>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user