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:
Aslam
2024-08-26 19:35:00 +07:00
committed by GitHub
parent 7cbfcc705b
commit 2d270706a5
77 changed files with 3002 additions and 1327 deletions

View 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} />
}

View File

@@ -0,0 +1,5 @@
import EditProfileRoute from "@/components/routes/EditProfileRoute"
export default function EditProfilePage() {
return <EditProfileRoute />
}

View File

@@ -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 />
}

View File

@@ -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
View File

@@ -0,0 +1,5 @@
import AuthHomeRoute from "@/components/routes/AuthHomeRoute"
export default function HomePage() {
return <AuthHomeRoute />
}

View File

@@ -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>
)
}

View File

@@ -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>
)
}

View File

@@ -1,5 +0,0 @@
import GlobalTopic from "@/components/routes/globalTopic/globalTopic"
export default function GlobalTopicPage({ params }: { params: { topic: string } }) {
return <GlobalTopic topic={params.topic} />
}

View File

@@ -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"
})
})

View File

@@ -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)

View File

@@ -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);
}
}

View File

@@ -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>
)
}