diff --git a/.env.example b/.env.example index 0c56f2de..21e8a7e4 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,6 @@ -ETERNALCODE_STRAPI_URL=https://localhost:1337 # Replace with your Strapi URL -ETERNALCODE_STRAPI_KEY=YOUR_STRAPI_KEY # Replace with your Strapi key \ No newline at end of file +# Server URL (required for PayloadCMS to generate correct media URLs) +NEXT_PUBLIC_SERVER_URL=http://localhost:3000 + +# PayloadCMS Configuration +PAYLOAD_SECRET=your-secret-key-that-is-very-long-and-secure +DATABASE_URI=file:./payload.db \ No newline at end of file diff --git a/.gitignore b/.gitignore index b88d084b..a9d927e1 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,8 @@ next-env.d.ts # ignore idea files .idea + +# Shannon +.shannon-tool +shannon_repos +shannon_results diff --git a/README.md b/README.md index 41f57b81..3fa6f76e 100644 --- a/README.md +++ b/README.md @@ -25,10 +25,4 @@ To learn more about Next.js, take a look at the following resources: You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! -## Deploy on Vercel -The easiest way to deploy your Next.js app is to use -the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) -from the creators of Next.js. - -Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/app/[...not-found]/page.tsx b/app/[...not-found]/page.tsx new file mode 100644 index 00000000..d330d70e --- /dev/null +++ b/app/[...not-found]/page.tsx @@ -0,0 +1,5 @@ +import { notFound } from "next/navigation"; + +export default function NotFoundCatchAll() { + notFound(); +} diff --git a/app/api/builds/builds.ts b/app/api/builds/builds.ts new file mode 100644 index 00000000..2dfbcc8b --- /dev/null +++ b/app/api/builds/builds.ts @@ -0,0 +1,170 @@ +import { z } from "zod"; + +export const BuildArtifactSchema = z.object({ + id: z.number(), + node_id: z.string(), + name: z.string(), + size_in_bytes: z.number(), + url: z.string(), + archive_download_url: z.string(), + expired: z.boolean(), + created_at: z.string(), + expires_at: z.string(), + updated_at: z.string(), +}); + +export type BuildArtifact = z.infer; + +export const BuildRunSchema = z.object({ + id: z.number(), + name: z.string().nullable(), + status: z.string(), + conclusion: z.string().nullable(), + head_branch: z.string(), + head_sha: z.string(), + created_at: z.string(), + html_url: z.string(), + artifacts_url: z.string(), + display_title: z.string().optional(), +}); + +export type BuildRun = z.infer & { + found_artifact?: BuildArtifact; +}; + +const GithubRunsResponseSchema = z.object({ + workflow_runs: z.array(BuildRunSchema), +}); + +const BuildArtifactsResponseSchema = z.object({ + total_count: z.number(), + artifacts: z.array(BuildArtifactSchema), +}); + +export const ModrinthFileSchema = z.object({ + url: z.string(), + filename: z.string(), + primary: z.boolean(), +}); + +export const ModrinthVersionSchema = z.object({ + id: z.string(), + name: z.string(), + version_number: z.string(), + date_published: z.string(), + files: z.array(ModrinthFileSchema), +}); + +export type ModrinthVersion = z.infer; + +export interface Project { + id: string; + name: string; + githubRepo: string; + modrinthId?: string; +} + +export const PROJECTS: Project[] = [ + { + id: "eternalcore", + name: "EternalCore", + githubRepo: "EternalCodeTeam/EternalCore", + modrinthId: "eternalcore", + }, + { + id: "eternalcombat", + name: "EternalCombat", + githubRepo: "EternalCodeTeam/EternalCombat", + modrinthId: "eternalcombat", + }, +]; + +export async function fetchDevBuilds(project: Project): Promise { + const token = process.env.GITHUB_TOKEN; + const headers: HeadersInit = { + Accept: "application/vnd.github.v3+json", + }; + + if (token) { + headers.Authorization = `token ${token}`; + } + + try { + const res = await fetch( + `https://api.github.com/repos/${project.githubRepo}/actions/runs?per_page=20&status=success&branch=master`, + { headers } + ); + if (!res.ok) { + console.error(`Failed to fetch Github Actions for ${project.name}`, await res.text()); + return []; + } + + const json = await res.json(); + const parsed = GithubRunsResponseSchema.safeParse(json); + + if (!parsed.success) { + console.error(`Invalid Github Actions response for ${project.name}`, parsed.error); + return []; + } + + const runs = parsed.data.workflow_runs; + + // Fetch artifacts for each run to get the correct artifact name + return await Promise.all( + runs.map(async (run) => { + try { + const artRes = await fetch(run.artifacts_url, { headers }); + if (!artRes.ok) { + return run; + } + + const artJson = await artRes.json(); + const artParsed = BuildArtifactsResponseSchema.safeParse(artJson); + + if (artParsed.success && artParsed.data.artifacts.length > 0) { + // We take the first artifact as the primary one + return { ...run, found_artifact: artParsed.data.artifacts[0] }; + } + return run; + } catch (e) { + console.error(`Error fetching artifacts for run ${run.id}`, e); + return run; + } + }) + ); + } catch (error) { + console.error(`Error fetching dev builds for ${project.name}`, error); + return []; + } +} + +export async function fetchStableBuilds(project: Project): Promise { + if (!project.modrinthId) { + return []; + } + + try { + const res = await fetch(`https://api.modrinth.com/v2/project/${project.modrinthId}/version`); + if (!res.ok) { + if (res.status === 404) { + return []; // Project might not exist yet + } + console.error(`Failed to fetch Modrinth versions for ${project.name}`, await res.text()); + return []; + } + + const json = await res.json(); + // Validate response is an array of ModrinthVersion + const parsed = z.array(ModrinthVersionSchema).safeParse(json); + + if (!parsed.success) { + console.error(`Invalid Modrinth versions response for ${project.name}`, parsed.error); + return []; + } + + return parsed.data; + } catch (error) { + console.error(`Error fetching stable builds for ${project.name}`, error); + return []; + } +} diff --git a/app/api/docs/search-index/route.ts b/app/api/docs/search-index/route.ts index e82bbea2..e432c483 100644 --- a/app/api/docs/search-index/route.ts +++ b/app/api/docs/search-index/route.ts @@ -4,6 +4,14 @@ import path from "node:path"; import matter from "gray-matter"; import { NextResponse } from "next/server"; +const MDX_EXTENSION_REGEX = /\.mdx$/; + +interface SearchIndexItem { + title: string; + path: string; + excerpt: string; +} + function findMarkdownFiles(dir: string): string[] { const files: string[] = []; const entries = fs.readdirSync(dir, { withFileTypes: true }); @@ -23,13 +31,13 @@ function findMarkdownFiles(dir: string): string[] { function generateSearchIndex() { const docsDir = path.join(process.cwd(), "content/docs"); const files = findMarkdownFiles(docsDir); - const searchIndex = []; + const searchIndex: SearchIndexItem[] = []; for (const file of files) { const content = fs.readFileSync(file, "utf8"); const { data, content: markdownContent } = matter(content); const relativePath = path.relative(docsDir, file); - const urlPath = `/docs/${relativePath.replace(/\.mdx$/, "")}`; + const urlPath = `/docs/${relativePath.replace(MDX_EXTENSION_REGEX, "")}`; const excerpt = markdownContent .replace(/[#*`_~]/g, "") @@ -47,7 +55,7 @@ function generateSearchIndex() { return searchIndex; } -export async function GET() { +export function GET() { try { const searchIndex = generateSearchIndex(); return NextResponse.json(searchIndex); diff --git a/app/api/og/route.tsx b/app/api/og/route.tsx index e3f1968a..409eeb58 100644 --- a/app/api/og/route.tsx +++ b/app/api/og/route.tsx @@ -1,4 +1,5 @@ -import { ImageResponse } from "@vercel/og"; +import { OgTemplate, loadFonts } from "@/components/og/og-template"; +import { ImageResponse } from "next/og"; import type { NextRequest } from "next/server"; export const runtime = "edge"; @@ -11,94 +12,18 @@ export async function GET(req: NextRequest) { const subtitle = searchParams.get("subtitle") || "Open Source Solutions"; const image = searchParams.get("image") || "https://eternalcode.pl/logo.svg"; - return new ImageResponse( -
-
- {/** biome-ignore lint/performance/noImgElement: it's for og image, i cant use here */} - EternalCode Logo -
-

- {title} -

-

- {subtitle} -

-
-
-
-

- eternalcode.pl -

-
-
, - { - width: 1200, - height: 630, - } - ); + const fonts = await loadFonts(); + + return new ImageResponse(, { + width: 1200, + height: 630, + fonts, + headers: { + "Cache-Control": "public, max-age=0, must-revalidate", + }, + }); } catch (e) { - console.error(e); + console.error("OG Image Generation Error:", e); return new Response("Failed to generate OG image", { status: 500 }); } } diff --git a/app/api/project/route.ts b/app/api/project/route.ts deleted file mode 100644 index c78d7bb4..00000000 --- a/app/api/project/route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NextResponse } from "next/server"; - -export const revalidate = 5; - -export async function GET(_request: Request) { - try { - const res = await fetch(`https://cms.eternalcode.pl/api/projects?populate=*`, { - headers: { - Authorization: `Bearer ${process.env.ETERNALCODE_STRAPI_KEY}`, - }, - next: { revalidate }, - }); - - if (!res.ok) { - const errorBody = await res.json(); - return NextResponse.json(errorBody, { status: res.status }); - } - - const body = await res.json(); - - return NextResponse.json(body, { - headers: { - "Cache-Control": "public, max-age=5, stale-while-revalidate=5", - }, - }); - } catch (error) { - console.error("Error fetching projects:", error); - return NextResponse.json({ error: "Failed to fetch projects" }, { status: 500 }); - } -} diff --git a/app/api/team/route.ts b/app/api/team/route.ts deleted file mode 100644 index 9c07fb5e..00000000 --- a/app/api/team/route.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NextResponse } from "next/server"; - -export const revalidate = 5; - -export async function GET(_request: Request) { - try { - const res = await fetch(`https://cms.eternalcode.pl/api/team-members?populate=*`, { - headers: { - Authorization: `Bearer ${process.env.ETERNALCODE_STRAPI_KEY}`, - }, - next: { revalidate }, - }); - - if (!res.ok) { - const errorBody = await res.json(); - return NextResponse.json(errorBody, { status: res.status }); - } - - const body = await res.json(); - - return NextResponse.json(body, { - headers: { - "Cache-Control": "public, max-age=5, stale-while-revalidate=5", - }, - }); - } catch (error) { - console.error("Error fetching team data:", error); - return NextResponse.json({ error: "Failed to fetch team data" }, { status: 500 }); - } -} diff --git a/app/author/[slug]/page.tsx b/app/author/[slug]/page.tsx deleted file mode 100644 index 8d55966c..00000000 --- a/app/author/[slug]/page.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { Pagination } from "@/components/ui/pagination"; -import { getAuthorBySlug, getBlogPostsByAuthor } from "@/lib/strapi"; -import BlogPostCard from "@/components/blog/BlogPostCard"; -import { getImageUrl } from "@/lib/utils"; -import Image from "next/image"; -import { SlideIn, StaggerContainer } from "@/components/ui/motion/MotionComponents"; -import { notFound } from "next/navigation"; - -interface AuthorPageProps { - params: Promise<{ slug: string }>; - searchParams: Promise<{ page?: string }>; -} - -export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }) { - const { slug } = await params; - const author = await getAuthorBySlug(slug); - - if (!author) { - return { - title: "Author Not Found | EternalCode.pl", - description: "This author does not exist on EternalCode.pl.", - }; - } - - return { - title: `${author.name} – Author | EternalCode.pl`, - description: author.bio || `Read articles by ${author.name} on EternalCode.pl`, - openGraph: { - title: `${author.name} – Author | EternalCode.pl`, - description: author.bio || `Read articles by ${author.name} on EternalCode.pl`, - type: "profile", - url: `https://eternalcode.pl/author/${author.slug}`, - images: author.avatar?.url ? [getImageUrl(author.avatar.url)] : [], - firstName: author.name.split(" ")[0], - lastName: author.name.split(" ").slice(1).join(" ") || undefined, - username: author.slug, - }, - twitter: { - card: "summary", - title: `${author.name} – Author | EternalCode.pl`, - description: author.bio || `Read articles by ${author.name} on EternalCode.pl`, - images: author.avatar?.url ? [getImageUrl(author.avatar.url)] : [], - }, - alternates: { - canonical: `https://eternalcode.pl/author/${author.slug}`, - }, - }; -} - -export default async function AuthorPage({ params, searchParams }: AuthorPageProps) { - const { slug } = await params; - const { page } = await searchParams; - - if (!slug) notFound(); - - const author = await getAuthorBySlug(slug); - if (!author) notFound(); - - const posts = await getBlogPostsByAuthor(slug); - const ITEMS_PER_PAGE = 6; - const currentPage = Math.max(1, parseInt(page || "1", 10)); - const totalPages = Math.ceil(posts.length / ITEMS_PER_PAGE); - const paginatedPosts = posts.slice( - (currentPage - 1) * ITEMS_PER_PAGE, - currentPage * ITEMS_PER_PAGE - ); - - return ( -
-
-
- - -
-

Articles

- {posts.length > 0 ? ( - <> - - {paginatedPosts.map((post, i) => ( - - - - ))} - - - - ) : ( -
- No articles found for this author (yet!) -
- )} -
-
-
-
- ); -} diff --git a/app/blog/[slug]/page.tsx b/app/blog/[slug]/page.tsx deleted file mode 100644 index 9ec2dd81..00000000 --- a/app/blog/[slug]/page.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import type { Metadata } from "next"; -import Image from "next/image"; -import Link from "next/link"; -import { notFound } from "next/navigation"; - -import { SlideIn } from "@/components/ui/motion/MotionComponents"; -import BlogPostContent from "@/components/blog/BlogPostContent"; -import { generateOgImageUrl } from "@/lib/og-utils"; -import { getBlogPost, type StrapiTag } from "@/lib/strapi"; - -export const dynamic = "force-dynamic"; -export const revalidate = 5; - -export async function generateStaticParams() { - try { - return []; - } catch (error) { - console.error("Error generating static params:", error); - return []; - } -} - -function getImageUrl(url: string) { - if (!url) return ""; - if (url.startsWith("http")) return url; - const base = process.env.NEXT_PUBLIC_ETERNALCODE_STRAPI_URL || ""; - return `${base}${url}`; -} - -function getTagsArray(tags: StrapiTag[] | { data: StrapiTag[] } | undefined): StrapiTag[] { - if (!tags) return []; - if (Array.isArray(tags)) return tags; - if ("data" in tags && Array.isArray(tags.data)) return tags.data; - return []; -} - -export async function generateMetadata({ - params, -}: { - params: Promise<{ slug: string }>; -}): Promise { - try { - const { slug } = await params; - const post = await getBlogPost(slug); - - if (!post) { - return { - title: "Post Not Found | EternalCode.pl", - }; - } - - const ogImageUrl = post.featuredImage?.url - ? getImageUrl(post.featuredImage.url) - : generateOgImageUrl({ - title: post.title, - subtitle: post.excerpt, - }); - - const tagsArr = getTagsArray(post.tags); - - return { - title: `${post.title} | EternalCode.pl`, - description: post.excerpt, - keywords: tagsArr.map((tag: StrapiTag) => tag.name) || [], - authors: [{ name: post.author?.name || "EternalCode Team" }], - openGraph: { - type: "article", - locale: "en_US", - url: `https://eternalcode.pl/blog/${post.slug}`, - siteName: "EternalCode.pl", - title: post.title, - description: post.excerpt, - images: [ - { - url: ogImageUrl, - width: 1200, - height: 630, - alt: post.title, - }, - ], - publishedTime: post.publishedAt, - modifiedTime: post.updatedAt, - authors: [post.author?.name || "EternalCode Team"], - tags: tagsArr.map((tag: StrapiTag) => tag.name) || [], - }, - twitter: { - card: "summary_large_image", - site: "@eternalcode", - creator: "@eternalcode", - title: post.title, - description: post.excerpt, - images: [ogImageUrl], - }, - alternates: { - canonical: `https://eternalcode.pl/blog/${post.slug}`, - }, - }; - } catch (error) { - console.error("Error generating metadata:", error); - return { - title: "Post Not Found | EternalCode.pl", - }; - } -} - -export default async function BlogPostPage({ params }: { params: Promise<{ slug: string }> }) { - try { - const { slug } = await params; - const post = await getBlogPost(slug); - - if (!post) { - notFound(); - } - - const tagsArr = getTagsArray(post.tags); - - return ( -
- {/* Hero Section */} - -
-

- {post.title} -

-

- {post.excerpt} -

-
- {post.author?.slug && ( - - {post.author.avatar && ( - {post.author.name} - )} - By {post.author.name} - - )} - - - {post.readingTime && ( - <> - - {post.readingTime} min read - - )} -
- {tagsArr.length > 0 && ( -
- {tagsArr.map((tag: StrapiTag) => ( - - {tag.name} - - ))} -
- )} -
-
- - {/* Blog Content */} - -
- -
-
-
- ); - } catch (error) { - console.error("Error fetching blog post:", error); - notFound(); - } -} diff --git a/app/blog/page.tsx b/app/blog/page.tsx deleted file mode 100644 index 0aea595c..00000000 --- a/app/blog/page.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { BookOpen, Users, Tag } from "lucide-react"; -import type { Metadata } from "next"; - -import { SlideIn, StaggerContainer } from "@/components/ui/motion/MotionComponents"; -import BlogPostCard from "@/components/blog/BlogPostCard"; -import { getBlogPosts, getBlogTags, getAuthors } from "@/lib/strapi"; - -export const dynamic = "force-dynamic"; -export const revalidate = 5; // Revalidate every 5 seconds - -export const metadata: Metadata = { - title: "Blog | EternalCode.pl", - description: "Discover the latest insights, tutorials, and articles from our team of experts.", - keywords: ["blog", "articles", "tutorials", "insights", "technology", "programming"], - openGraph: { - type: "website", - locale: "en_US", - url: "https://eternalcode.pl/blog", - siteName: "EternalCode.pl", - title: "Blog | EternalCode.pl", - description: "Discover the latest insights, tutorials, and articles from our team of experts.", - images: [ - { - url: "https://eternalcode.pl/api/og?title=Blog&subtitle=Latest insights and tutorials", - width: 1200, - height: 630, - alt: "EternalCode Blog", - }, - ], - }, - twitter: { - card: "summary_large_image", - site: "@eternalcode", - creator: "@eternalcode", - title: "Blog | EternalCode.pl", - description: "Discover the latest insights, tutorials, and articles from our team of experts.", - images: ["https://eternalcode.pl/api/og?title=Blog&subtitle=Latest insights and tutorials"], - }, - alternates: { - canonical: "https://eternalcode.pl/blog", - }, -}; - -export default async function BlogPage() { - const posts = await getBlogPosts(); - const tags = await getBlogTags(); - const authors = await getAuthors(); - - return ( -
- -
-
-
-

- Blog -

-
- - {posts.length} articles - - {authors.length > 0 && ( - - {authors.length} authors - - )} - - {tags.length} topics - -
-
-
-

- Discover the latest insights, tutorials, and articles from our team of experts. -

-
-
-
-
- -
-
- {posts.length > 0 ? ( - - {posts.map((post, i) => ( - - - - ))} - - ) : ( -
- -

- No articles yet -

-

- We're working on some great content. Check back soon! -

-
- )} -
-
-
- ); -} diff --git a/app/builds/page.tsx b/app/builds/page.tsx new file mode 100644 index 00000000..29214fde --- /dev/null +++ b/app/builds/page.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { fetchDevBuilds, fetchStableBuilds, PROJECTS, type Project } from "@/app/api/builds/builds"; +import { BuildControls } from "@/components/builds/build-controls"; +import { BuildHeader } from "@/components/builds/build-header"; +import type { Build } from "@/components/builds/build-row"; +import { BuildTable } from "@/components/builds/build-table"; +import { FacadePattern } from "@/components/ui/facade-pattern"; +import { Loader2 } from "lucide-react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { Suspense, useEffect, useState } from "react"; + +function BuildExplorerContent() { + const searchParams = useSearchParams(); + const router = useRouter(); + + const projectIdParam = searchParams.get("project"); + const initialProject = PROJECTS.find((p) => p.id === projectIdParam) || PROJECTS[0]; + + const [activeProject, setActiveProject] = useState(initialProject); + const [activeTab, setActiveTab] = useState<"STABLE" | "DEV">("STABLE"); + const [builds, setBuilds] = useState([]); + const [loading, setLoading] = useState(true); + const [lastDownloadedId, setLastDownloadedId] = useState(null); + + useEffect(() => { + const foundProject = PROJECTS.find((p) => p.id === projectIdParam); + if (foundProject && foundProject.id !== activeProject.id) { + setActiveProject(foundProject); + } + }, [projectIdParam, activeProject.id]); + + useEffect(() => { + const stored = localStorage.getItem(`last_download_${activeProject.id}`); + setLastDownloadedId(stored); + }, [activeProject.id]); + + const handleProjectChange = (projectId: string) => { + const project = PROJECTS.find((p) => p.id === projectId); + if (!project) { + return; + } + + setActiveProject(project); + const params = new URLSearchParams(searchParams.toString()); + params.set("project", project.id); + router.push(`/builds?${params.toString()}`); + }; + + useEffect(() => { + async function fetchData() { + setLoading(true); + setBuilds([]); // Clear previous builds + try { + if (activeTab === "STABLE") { + const data = await fetchStableBuilds(activeProject); + setBuilds( + data.map((version) => ({ + id: version.id, + name: version.name, + type: "STABLE", + date: version.date_published, + downloadUrl: version.files?.[0]?.url || "", + version: version.version_number, + })) + ); + } else { + const runs = await fetchDevBuilds(activeProject); + setBuilds( + runs.map((run) => { + const displayTitle = run.display_title; + const artifactName = run.found_artifact?.name || `${activeProject.name} Dev Build`; + + return { + id: run.id.toString(), + name: displayTitle || run.name || `Run #${run.id}`, + type: "DEV", + date: run.created_at, + downloadUrl: `https://nightly.link/${ + activeProject.githubRepo + }/actions/runs/${run.id}/${encodeURIComponent(artifactName)}.zip`, + commit: run.head_sha.substring(0, 7), + runUrl: run.html_url, + }; + }) + ); + } + } catch (e) { + console.error("Failed to load builds", e); + } finally { + setLoading(false); + } + } + fetchData(); + }, [activeTab, activeProject]); + + return ( +
+ {/* Background Decor */} +
+
+
+
+ +
+ +
+ + + + + { + localStorage.setItem(`last_download_${activeProject.id}`, id); + setLastDownloadedId(id); + }} + project={activeProject} + /> +
+
+ ); +} + +export default function BuildExplorerPage() { + return ( + + +
+ } + > + + + ); +} diff --git a/app/contribute/contribute-view.tsx b/app/contribute/contribute-view.tsx new file mode 100644 index 00000000..0a73613f --- /dev/null +++ b/app/contribute/contribute-view.tsx @@ -0,0 +1,68 @@ +"use client"; + +import { motion } from "framer-motion"; +import { ContributeHero } from "@/components/contribute/contribute-hero"; +import { ContributionCard } from "@/components/contribute/contribution-card"; +import { ContributionEmptyState } from "@/components/contribute/contribution-empty-state"; +import { ContributionHint } from "@/components/contribute/contribution-hint"; + +const containerVariants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + }, + }, +}; + +export interface ContributionCardData { + id?: string | null; + title: string; + description: string; + icon?: string | null; + actionText: string; + href: string; + color: string; +} + +export default function ContributeView({ cards }: { cards: ContributionCardData[] }) { + return ( +
+ {/* Background decoration */} +
+
+
+
+ + + +
+ {cards.length > 0 ? ( + + {cards.map((card, index) => ( + + ))} + + ) : ( + + )} + + +
+
+ ); +} diff --git a/app/contribute/page.tsx b/app/contribute/page.tsx new file mode 100644 index 00000000..5cf15303 --- /dev/null +++ b/app/contribute/page.tsx @@ -0,0 +1,53 @@ +import ContributeView from "./contribute-view"; + +export const metadata = { + title: "Contribute | EternalCode", + description: + "Join the EternalCode community. Contribute code, support us financially, or help with documentation and support.", +}; + +// Define the type locally since we removed payload-types-generated +interface CardData { + id: string; + title: string; + description: string; + icon: string; + actionText: string; + href: string; + color: string; +} + +export default function ContributePage() { + // Static data replacement + const cards: CardData[] = [ + { + id: "1", + title: "Code", + description: "Help us build the future of our projects by contributing code.", + icon: "Code2", + actionText: "Check Repositories", + href: "https://github.com/EternalCodeTeam", + color: "#3b82f6", + }, + { + id: "2", + title: "Discord", + description: "Join our community to discuss ideas, get help, and hang out.", + icon: "MessageCircle", + actionText: "Join Discord", + href: "https://discord.gg/eternalcode", + color: "#5865F2", + }, + { + id: "3", + title: "Sponsor", + description: "Support our work financially to help us keep the servers running.", + icon: "Heart", + actionText: "Sponsor Us", + href: "https://github.com/sponsors/EternalCodeTeam", + color: "#ec4899", + }, + ]; + + return ; +} diff --git a/app/docs/[...slug]/page.tsx b/app/docs/[...slug]/page.tsx index bb41e80b..902c2b22 100644 --- a/app/docs/[...slug]/page.tsx +++ b/app/docs/[...slug]/page.tsx @@ -1,18 +1,17 @@ import fs from "node:fs/promises"; import path from "node:path"; -import { Suspense } from "react"; - import matter from "gray-matter"; import type { Metadata } from "next"; import { notFound } from "next/navigation"; import { MDXRemote } from "next-mdx-remote/rsc"; +import { Suspense } from "react"; -import { DocsHeader } from "@/components/docs/content/DocsHeader"; -import { DocsNavigation } from "@/components/docs/content/DocsNavigation"; -import { EditOnGitHub } from "@/components/docs/content/EditOnGitHub"; -import { ErrorBoundary } from "@/components/docs/content/ErrorBoundary"; -import { ReadingTime } from "@/components/docs/content/ReadingTime"; -import { components, mdxOptions } from "@/components/mdx/mdx-components"; +import { DocsHeader } from "@/components/docs/content/docs-header"; +import { DocsNavigation } from "@/components/docs/content/docs-navigation"; +import { EditOnGitHub } from "@/components/docs/content/edit-on-github"; +import { ErrorBoundary } from "@/components/docs/content/error-boundary"; +import { ReadingTime } from "@/components/docs/content/reading-time"; +import { components, mdxOptions } from "@/components/ui/mdx/mdx-components"; import { docsStructure } from "@/lib/sidebar-structure"; interface DocMeta { @@ -133,13 +132,13 @@ function LoadingFallback() {
{skeletonKeys.map((key) => ( -
+
))}
); } -export async function generateStaticParams() { +export function generateStaticParams() { const flatDocs = getFlatDocs(); return flatDocs.map((doc) => ({ slug: doc.path.replace("/docs/", "").split("/"), @@ -149,7 +148,9 @@ export async function generateStaticParams() { export default async function DocPage({ params }: Props) { const resolvedParams = await params; const doc = await getDocBySlug(resolvedParams.slug); - if (!doc) notFound(); + if (!doc) { + notFound(); + } const currentPath = `/docs/${resolvedParams.slug.join("/")}`; const { prev, next } = getDocNavigation(currentPath); @@ -158,30 +159,35 @@ export default async function DocPage({ params }: Props) { return (
-
+
} + category={category} + description={doc.meta.description} + icon={doc.meta.icon} + title={doc.meta.title} /> -
+
}> - +
- +
); } diff --git a/app/docs/layout.tsx b/app/docs/layout.tsx index 7f5abe0a..9f3703e4 100644 --- a/app/docs/layout.tsx +++ b/app/docs/layout.tsx @@ -1,8 +1,7 @@ import type { Metadata } from "next"; import { Poppins } from "next/font/google"; - -import SidebarWrapper from "@/components/docs/sidebar/SidebarWrapper"; import type { ReactNode } from "react"; +import SidebarWrapper from "@/components/docs/sidebar/sidebar-wrapper"; const poppins = Poppins({ weight: "500", @@ -35,12 +34,21 @@ export const metadata: Metadata = { }, }; +import { FacadePattern } from "@/components/ui/facade-pattern"; + export default function DocsLayout({ children }: { children: ReactNode }) { return (
-
+ {/* Background Decor */} +
+
+
+ +
+ +
diff --git a/app/globals.css b/app/globals.css index 21894b92..6adadd26 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,6 +1,6 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; +@import "../node_modules/@south-paw/typeface-minecraft/index.css" layer(base); +@import "./prism-tomorrow.css" layer(base); @layer base { :root { @@ -77,36 +77,44 @@ } } +/* Moved generic block here to fix descending specificity and Biome errors */ +pre, +code, +.prose pre, +.prose code { + font-family: + "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace !important; +} + code[class*="language-"], pre[class*="language-"] { color: var(--prism-text, #24292e) !important; background-color: var(--prism-bg, #ffffff) !important; text-shadow: none !important; - font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace !important; + font-family: + Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace !important; } -code[class*="language-"]>span:not([class]) { +code[class*="language-"] > span:not([class]) { color: var(--prism-text, #24292e) !important; } -:not(pre)>code[class*="language-"], +:not(pre) > code[class*="language-"], pre[class*="language-"] { background: var(--prism-bg, #ffffff) !important; } -@import '../node_modules/@south-paw/typeface-minecraft/index.css' layer(base); -@import './prism-tomorrow.css' layer(base); -@import 'tailwindcss'; - @custom-variant dark (&:is(.dark *)); @theme { --font-monospace: - 'JetBrains Mono', ui-monospace, SFMono-Regular, Menlo, monospace; + "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; --background-image-gradient-radial: radial-gradient(var(--tw-gradient-stops)); - --background-image-gradient-conic: conic-gradient(from 180deg at 50% 50%, - var(--tw-gradient-stops)); + --background-image-gradient-conic: conic-gradient( + from 180deg at 50% 50%, + var(--tw-gradient-stops) + ); --color-gray-50: #f7f8fa; --color-gray-100: #eef1f5; @@ -153,7 +161,6 @@ pre[class*="language-"] { color utility to any element that depends on these defaults. */ @layer base { - *, ::after, ::before, @@ -164,7 +171,6 @@ pre[class*="language-"] { } @utility scrollbar-hide { - /* Hide scrollbar for Chrome, Safari and Opera */ &::-webkit-scrollbar { display: none; @@ -177,9 +183,15 @@ pre[class*="language-"] { /* Firefox */ } +@utility optimize-visibility { + content-visibility: auto; + contain-intrinsic-size: 100px; +} + code[class*="language-"], pre[class*="language-"] { - font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace !important; + font-family: + "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace !important; font-size: 1em; text-align: left; white-space: pre; @@ -200,7 +212,7 @@ pre[class*="language-"] { border-radius: 0.375rem; } -:not(pre)>code[class*="language-"] { +:not(pre) > code[class*="language-"] { padding: 0.1em; border-radius: 0.25rem; white-space: normal; @@ -212,7 +224,7 @@ pre[class*="language-"].line-numbers { counter-reset: linenumber; } -pre[class*="language-"].line-numbers>code { +pre[class*="language-"].line-numbers > code { position: relative; white-space: inherit; } @@ -229,12 +241,12 @@ pre[class*="language-"].line-numbers>code { user-select: none; } -.line-numbers-rows>span { +.line-numbers-rows > span { display: block; counter-increment: linenumber; } -.line-numbers-rows>span:before { +.line-numbers-rows > span:before { content: counter(linenumber); color: #999; display: block; @@ -277,6 +289,22 @@ pre[class*="language-"].line-numbers>code { } } +@keyframes slide-up { + from { + opacity: 0; + transform: translateY(20px); + } + + to { + opacity: 1; + transform: none; + } +} + +.animate-slide-up { + animation: slide-up 0.6s cubic-bezier(0.2, 0.8, 0.2, 1) both; +} + .animate-fadein { animation: fadein 0.4s cubic-bezier(0.4, 2, 0.6, 1) both; } @@ -314,20 +342,14 @@ pre { font-weight: 600; } -pre, -code, -.prose pre, -.prose code { - font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace !important; -} - code[class*="language-"], pre[class*="language-"] { text-shadow: none !important; - font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace !important; + font-family: + Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace !important; } -:not(pre)>code[class*="language-"], +:not(pre) > code[class*="language-"], pre[class*="language-"] { color: var(--prism-text) !important; -} \ No newline at end of file +} diff --git a/app/layout.tsx b/app/layout.tsx index 444086c5..0fe0518c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,16 +2,13 @@ import type { Metadata, Viewport } from "next"; import { Poppins } from "next/font/google"; import NextTopLoader from "nextjs-toploader"; import "./globals.css"; +import "lenis/dist/lenis.css"; import type React from "react"; -import { Analytics } from "@/components/Analytics"; import "./prism-languages"; -import { CookieConsentModal } from "@/components/CookieConsentModal"; -import { CookiePreferencesMenu } from "@/components/CookiePreferencesMenu"; -import Footer from "@/components/footer/Footer"; -import Navbar from "@/components/hero/Navbar"; -import { SpeedInsights } from "@/components/SpeedInsights"; -import { generateOgImageUrl } from "@/lib/og-utils"; +import { CookieConsentModal } from "@/components/cookie-consent-modal"; +import Footer from "@/components/footer/footer"; +import Navbar from "@/components/hero/navbar"; import { Providers } from "./providers"; @@ -38,11 +35,6 @@ export const viewport: Viewport = { ], }; -const defaultOgImageUrl = generateOgImageUrl({ - title: "EternalCode.pl", - subtitle: "We are a team creating open source projects!", -}); - export const metadata: Metadata = { metadataBase: new URL("https://eternalcode.pl"), title: "EternalCode.pl | Home", @@ -65,14 +57,6 @@ export const metadata: Metadata = { title: "EternalCode.pl | We are a team creating open source projects!", description: "EternalCode.pl delivers open source solutions with a focus on quality, performance, and innovation.", - images: [ - { - url: defaultOgImageUrl, - width: 1200, - height: 630, - alt: "EternalCode.pl", - }, - ], }, twitter: { card: "summary_large_image", @@ -81,7 +65,6 @@ export const metadata: Metadata = { title: "EternalCode.pl | We are a team creating open source projects!", description: "EternalCode.pl delivers open source solutions with a focus on quality, performance, and innovation.", - images: [defaultOgImageUrl], }, robots: { index: true, @@ -105,21 +88,21 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - +
@@ -131,9 +114,6 @@ export default function RootLayout({