+ {article.title} +
+ + {/* Excerpt */} + {article.excerpt && ( ++ {article.excerpt} +
+ )} + + {/* Thumbnail image */} + {ensureHttps(article.imageUrl) && article.externalUrl && ( + ++ {article.source.description} +
+ )} +diff --git a/app/(app)/[username]/[slug]/_feedArticleContent.tsx b/app/(app)/[username]/[slug]/_feedArticleContent.tsx new file mode 100644 index 00000000..dbe9e19a --- /dev/null +++ b/app/(app)/[username]/[slug]/_feedArticleContent.tsx @@ -0,0 +1,439 @@ +"use client"; + +import Link from "next/link"; +import * as Sentry from "@sentry/nextjs"; +import { + ArrowTopRightOnSquareIcon, + BookmarkIcon, + ChatBubbleLeftIcon, + ChevronUpIcon, + ChevronDownIcon, + ShareIcon, +} from "@heroicons/react/20/solid"; +import { BookmarkIcon as BookmarkOutlineIcon } from "@heroicons/react/24/outline"; +import { api } from "@/server/trpc/react"; +import { signIn, useSession } from "next-auth/react"; +import { toast } from "sonner"; +import { Temporal } from "@js-temporal/polyfill"; +import DiscussionArea from "@/components/Discussion/DiscussionArea"; + +type Props = { + sourceSlug: string; + articleSlug: string; +}; + +// Get favicon URL from a website +const getFaviconUrl = ( + websiteUrl: string | null | undefined, +): string | null => { + if (!websiteUrl) return null; + try { + const url = new URL(websiteUrl); + return `https://www.google.com/s2/favicons?domain=${url.hostname}&sz=32`; + } catch { + return null; + } +}; + +// Get hostname from URL +const getHostname = (urlString: string): string => { + try { + const url = new URL(urlString); + return url.hostname; + } catch { + return urlString; + } +}; + +// Ensure image URL uses https +const ensureHttps = (url: string | null | undefined): string | null => { + if (!url) return null; + if (url.startsWith("http://")) { + return url.replace("http://", "https://"); + } + return url; +}; + +const FeedArticleContent = ({ sourceSlug, articleSlug }: Props) => { + const { data: session } = useSession(); + const utils = api.useUtils(); + + const { data: article, status } = api.feed.getBySourceAndArticleSlug.useQuery( + { + sourceSlug, + articleSlug, + }, + ); + + const { data: discussionCount } = + api.discussion.getContentDiscussionCount.useQuery( + { contentId: article?.id ?? "" }, + { enabled: !!article?.id }, + ); + + const { mutate: vote, status: voteStatus } = api.content.vote.useMutation({ + onSuccess: () => { + utils.feed.getBySourceAndArticleSlug.invalidate({ + sourceSlug, + articleSlug, + }); + utils.content.getFeed.invalidate(); + }, + onError: (error) => { + toast.error("Failed to update vote"); + Sentry.captureException(error); + }, + }); + + const { mutate: bookmark, status: bookmarkStatus } = + api.feed.bookmark.useMutation({ + onSuccess: () => { + utils.feed.getBySourceAndArticleSlug.invalidate({ + sourceSlug, + articleSlug, + }); + utils.feed.getFeed.invalidate(); + utils.feed.mySavedArticles.invalidate(); + }, + onError: (error) => { + toast.error("Failed to update bookmark"); + Sentry.captureException(error); + }, + }); + + const { mutate: trackClick } = api.feed.trackClick.useMutation(); + + const handleVote = (voteType: "up" | "down" | null) => { + if (!session) { + signIn(); + return; + } + if (article) { + vote({ contentId: article.id, voteType }); + } + }; + + const handleBookmark = () => { + if (!session) { + signIn(); + return; + } + if (article) { + bookmark({ articleId: article.id, setBookmarked: !article.isBookmarked }); + } + }; + + const handleShare = async () => { + const shareUrl = `${window.location.origin}/${sourceSlug}/${articleSlug}`; + try { + await navigator.clipboard.writeText(shareUrl); + toast.success("Link copied to clipboard"); + } catch { + toast.error("Failed to copy link"); + } + }; + + const handleExternalClick = () => { + if (article) { + trackClick({ articleId: article.id }); + } + }; + + if (status === "pending") { + return ( +
+ This post may have been removed or the link is invalid. +
++ {article.excerpt} +
+ )} + + {/* Thumbnail image */} + {ensureHttps(article.imageUrl) && article.externalUrl && ( + ++ {article.source.description} +
+ )} ++ This link may have been removed or the URL is invalid. +
++ {linkContent.excerpt} +
+ )} + + {/* Thumbnail image */} + {ensureHttps(linkContent.imageUrl) && externalUrl && ( + ++ {linkContent.source.description} +
+ )} ++ Comments are disabled for this post +
++ Comments are disabled for this article +
++ This source may have been removed or the link is invalid. +
+ + Back to Feed + +{source.description || ""}
+ {source.websiteUrl && ( + ++ {getDomainFromUrl(source.websiteUrl)} +
+ + )} +Nothing published yet... 🥲
+ ) : ( + <> + {articles.map((article) => { + // Use slug for SEO-friendly URLs, fallback to shortId for legacy articles + const articleSlug = article.slug || article.shortId; + + return ( ++ {title} +
++ {isLoading ? ( + + ) : ( + (value ?? 0) + )} +
++ Manage and monitor the Codú platform +
++ Moderation Queue +
++ Review reported content +
++ User Management +
++ Search and manage users +
++ Feed Sources +
++ Manage RSS feed sources +
++ Review and manage reported content +
++ {statusFilter + ? `No ${statusFilter.toLowerCase()} reports` + : "All caught up!"} +
++ {report.content.type} by @{report.content.user?.username} +
++ {report.content.title} +
++ Comment by @{report.discussion.user?.username} +
++ {report.discussion.body} +
++ Details: {report.details} +
++ Reported by @{report.reporter?.username || "unknown"} +
+ + {/* Actions */} + {report.status === "PENDING" && ( ++ Reviewed by @{report.reviewedBy.username} +
+ )} ++ Manage RSS feed sources for the content aggregator +
+| + Source + | ++ Category + | ++ Status + | ++ Articles + | ++ Last Fetched + | ++ Errors + | ++ Actions + | +
|---|---|---|---|---|---|---|
|
+
+ {source.sourceName}
+
+ |
+ + {/* Category would need to be fetched separately or added to stats */} + - + | +
+
+ |
+ + {source.articleCount} + | ++ {source.lastFetchedAt + ? new Date(source.lastFetchedAt).toLocaleDateString() + : "Never"} + | ++ {source.errorCount} + | +
+
+
+
+
+
+ |
+
+ Search and manage platform users +
++ {search + ? "Try a different search term" + : showBannedOnly + ? "No banned users" + : "No users yet"} +
++ @{user.username} · {user.email} + {"createdAt" in user && user.createdAt && ( + <> · Joined {getRelativeTime(user.createdAt)}> + )} +
++ Ban reason:{" "} + {user.banNote} +
+ {"bannedBy" in user && user.bannedBy && ( ++ Banned by @{user.bannedBy.username} +
+ )} +- Codú aims to create one of the largest coding communities - globally. Your funds go directly towards building the community - and a flourishing ecosystem. -
-- We offer opportunities to sponsor hackathons, monthly{" "} - events, giveaways and online ad space. -
-- - Contact us - {" "} - today to find out more. -
-+ {excerpt} +
+ )} + + {/* Action bar */} ++ Check back later for new content. +
++ This post may have been removed or the link is invalid. +
++ {article.excerpt} +
+ )} + + {/* Thumbnail image */} + {ensureHttps(article.imageUrl) && ( + ++ This source may have been removed or the link is invalid. +
+ + Back to Feed + +{source.description || ""}
+ {source.websiteUrl && ( + ++ {getDomainFromUrl(source.websiteUrl)} +
+ + )} +Nothing published yet... 🥲
+ ) : ( + <> + {articles.map((article) => { + // Use slug for SEO-friendly URLs, fallback to shortId for legacy articles + const articleSlug = article.slug || article.shortId; + + return ( ++ Check back soon for curated developer content. +
++ Curated developer content from across the web. Upvote articles you + find helpful, save them for later, and discover trending topics in + the developer community. +
++ No saved articles yet. Save articles to read them later! +
+ ); + } + + // Map DB type to frontend type + const toFrontendType = (dbType: string | null): "POST" | "LINK" => { + if (dbType === "article") return "POST"; + return "LINK"; + }; + + return ( +{excerpt || "No excerpt yet... Write more to see one."}
-- Read time so far: {readTimeMins} mins -
Something went wrong fetching your saved posts... Refresh the page.
@@ -40,40 +50,22 @@ const SavedPosts = () => {
)}
{bookmarkStatus === "success" &&
- bookmarks.map(
- ({
- id,
- slug,
- title,
- excerpt,
- user: { name, image, username },
- updatedAt,
- readTimeMins,
- }) => {
- return (
-
diff --git a/app/(app)/sponsorship/page.tsx b/app/(app)/sponsorship/page.tsx
index ecdc46b2..4ed5eaee 100644
--- a/app/(app)/sponsorship/page.tsx
+++ b/app/(app)/sponsorship/page.tsx
@@ -1,133 +1,5 @@
-import Link from "next/link";
+import { permanentRedirect } from "next/navigation";
-export const metadata = {
- title: "Sponsor Codú - And Reach More Developers!",
- description:
- "The largest JavaScript and web developer community in Ireland! Reach thousands of developers in Ireland and beyond.",
-};
-
-const Sponsorship = () => {
- return (
- <>
-
- Our work and events would not be possible without the support of
- our partners.
-
- {`Codú is the perfect place to show your company's support of open source software, find new developers and fund the next generation of avid learners.`}
-
- {`Sponsors can post jobs to our network of thousands of developers (and growing), brand at our events, and advertise in our newsletter.`}
-
- For more info contact us:
-
- Support us
-
-
- Sponsor Codú
-
-
-
- Previous sponsors include
-
-
-
-
-
-
-
-
-
-
-
-