Skip to content

137 create reusable organizationcard component#174

Open
DankaMarci wants to merge 3 commits into
mainfrom
137-create-reusable-organizationcard-component
Open

137 create reusable organizationcard component#174
DankaMarci wants to merge 3 commits into
mainfrom
137-create-reusable-organizationcard-component

Conversation

@DankaMarci

@DankaMarci DankaMarci commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Organization pages now share a consistent card layout with clearer sections for introductions, events, activities, departments, galleries, contacts, and join links.
    • Social links are displayed with better icon and label handling, including improved support for email and website links.
  • Bug Fixes

    • Improved display of organization data by standardizing field formatting and preserving content across different page types.

@DankaMarci
DankaMarci requested a review from peterlipt July 8, 2026 18:56
@DankaMarci DankaMarci self-assigned this Jul 8, 2026
@vercel

vercel Bot commented Jul 8, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
ehk Ready Ready Preview, Comment Jul 8, 2026 6:56pm

Request Review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

A new OrganizationCard component and its supporting section subcomponents, types, and utilities were introduced under src/components/common/organization-card/. Three Közélet pages were refactored to normalize their dictionary data into OrganizationCardProps and render this shared component instead of bespoke inline card/gallery/social-link UI. social-utils.tsx was updated with locally defined SVG icons and expanded label-matching logic.

Changes

OrganizationCard extraction and adoption

Layer / File(s) Summary
Organization card types and utilities
src/components/common/organization-card/types.ts, src/components/common/organization-card/utils.tsx
Defines TextContent, OrganizationStat, OrganizationEvent, OrganizationGalleryImage, OrganizationSocialLink, OrganizationCardLabels, and OrganizationCardProps types, plus helper functions for localized labels, text rendering, image src/alt resolution, and stat label formatting.
Section subcomponents
src/components/common/organization-card/sections/*
Adds Section, OrganizationCardHeader, PresentationSection/RichTextSection, InlineListSection/DepartmentsSection, EventsSection, GallerySection, ContactsSection, JoinCta, and a barrel index.ts re-exporting them.
OrganizationCard component assembly
src/components/common/OrganizationCard.tsx
Composes all sections into a single OrganizationCard component, merging locale-based labels and sorting social links before rendering header, content sections, and a join CTA.
Social icon and label utilities update
src/lib/social-utils.tsx
Replaces several lucide-react icons with local SVG components, adds a className param to getSocialIcon, and extends getSocialPriority/getSocialName to handle email-like and domain-like labels.
Page refactors to use OrganizationCard
src/app/(app)/[lang]/kozelet/ontevekenykorok/page.tsx, .../szakkollegiumok/page.tsx, .../versenycsapatok/page.tsx
Replaces inline card/gallery/social-link rendering with Organization type definitions, getContactLabel/getOrganizationCardProps helpers, and mapped OrganizationCard rendering for each page's organization data.

Estimated code review effort: 3 (Moderate) | ~35 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Page as Közélet Page
  participant Helper as getOrganizationCardProps
  participant OrganizationCard
  participant Sections as Section Subcomponents

  Page->>Helper: normalize dictionary org data
  Helper-->>Page: OrganizationCardProps
  Page->>OrganizationCard: render(props)
  OrganizationCard->>Sections: render header, events, gallery, contacts, join CTA
  Sections-->>OrganizationCard: rendered JSX
  OrganizationCard-->>Page: composed card markup
Loading

Possibly related issues

Suggested reviewers: peterlipt

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title matches the main change: introducing a reusable OrganizationCard component.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch 137-create-reusable-organizationcard-component

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
src/lib/social-utils.tsx (1)

120-201: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract repeated label-matching conditions into shared helpers.

The web/domain, email, and X/Twitter label-matching conditions are each duplicated across getSocialIcon, getSocialPriority, and getSocialName. Adding a new TLD or adjusting email detection requires changes in three places. Extracting these into shared predicates eliminates the triplication and ensures consistency.

♻️ Proposed refactor: shared label predicates
+function isWebLabel(l: string): boolean {
+  return (
+    l.includes("weblap") ||
+    l.includes("web") ||
+    l.includes("honlap") ||
+    l.includes("mvk.bme.hu") ||
+    l.includes(".hu") ||
+    l.includes(".eu") ||
+    l.includes(".com") ||
+    l.includes(".org")
+  );
+}
+
+function isEmailLabel(l: string): boolean {
+  return l.includes("@") || l.includes("email") || l.includes("e-mail");
+}
+
+function isXTwitterLabel(l: string): boolean {
+  return l === "x" || l.includes("twitter");
+}
+
 export function getSocialIcon(label: string, className = "h-4 w-4") {
   const l = label.toLowerCase();
   if (l.includes("facebook")) return <FacebookIcon className={className} />;
   if (l.includes("instagram")) return <InstagramIcon className={className} />;
   if (l.includes("linkedin")) return <LinkedInIcon className={className} />;
   if (l.includes("youtube")) return <YouTubeIcon className={className} />;
   if (l.includes("tiktok")) return <TiktokIcon className={className} />;
-  if (l === "x" || l.includes("twitter")) {
+  if (isXTwitterLabel(l)) {
     return <XSocialIcon className={className} />;
   }
   if (l.includes("linktr.ee")) return <LinktreeIcon className={className} />;
   if (l.includes("threads")) return <ThreadsIcon className={className} />;
-  if (l.includes("@") || l.includes("email") || l.includes("e-mail")) {
+  if (isEmailLabel(l)) {
     return <Mail className={className} />;
   }
-  if (
-    l.includes("weblap") ||
-    l.includes("web") ||
-    l.includes("honlap") ||
-    l.includes("mvk.bme.hu") ||
-    l.includes(".hu") ||
-    l.includes(".eu") ||
-    l.includes(".com") ||
-    l.includes(".org")
-  ) {
+  if (isWebLabel(l)) {
     return <Globe className={className} />;
   }
   return <ExternalLink className={className} />;
 }

Apply the same predicates in getSocialPriority and getSocialName to replace their inline copies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/social-utils.tsx` around lines 120 - 201, The label-matching logic
for web/domain, email, and X/Twitter is duplicated in getSocialIcon,
getSocialPriority, and getSocialName, so extract shared predicate helpers in
social-utils.tsx and reuse them from all three functions. Update the matching in
getSocialPriority and getSocialName to call the same helpers used by
getSocialIcon so any future TLD or email rule change only needs one edit and
stays consistent.
src/app/(app)/[lang]/kozelet/ontevekenykorok/page.tsx (1)

9-53: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract duplicated Organization type and helpers to a shared module.

The Organization type, getContactLabel, and getOrganizationCardProps are duplicated across all three refactored page files (ontevekenykorok, szakkollegiumok, versenycsapatok). The only meaningful difference is imageBasePath handling — hardcoded here, parameterized elsewhere. Extracting these to a shared utility module under organization-card/ would eliminate ~50 lines of triplicated code and ensure consistent prop normalization.

♻️ Proposed shared module
// src/components/common/organization-card/organization-utils.ts
import type { OrganizationCardProps } from "./types";
import type { Locale } from "`@/i18n-config`";

export type Organization = {
  id: string;
  title: string;
  description: string[];
  social_title: string;
  social_links: { label: string; url: string }[];
  images?: string[];
  stats?: OrganizationCardProps["stats"];
  events?: OrganizationCardProps["events"];
  activities?: OrganizationCardProps["activities"];
  departments?: OrganizationCardProps["departments"];
  target_audience?: OrganizationCardProps["targetAudience"];
  targetAudience?: OrganizationCardProps["targetAudience"];
  join_url?: string;
  joinUrl?: string;
  join_text?: string;
  joinText?: string;
};

export function getContactLabel(label: string) {
  return label.replace(/:$/, "");
}

export function getOrganizationCardProps(
  organization: Organization,
  locale: Locale,
  imageBasePath?: string,
): OrganizationCardProps {
  return {
    name: organization.title,
    stats: organization.stats,
    presentation: organization.description,
    events: organization.events,
    activities: organization.activities,
    departments: organization.departments,
    targetAudience:
      organization.targetAudience ?? organization.target_audience,
    socialLinks: organization.social_links,
    galleryImages: organization.images,
    imageBasePath,
    joinUrl: organization.joinUrl ?? organization.join_url,
    joinText: organization.joinText ?? organization.join_text,
    labels: { contacts: getContactLabel(organization.social_title) },
    locale,
  };
}

Then each page imports from this module instead of redefining locally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/app/`(app)/[lang]/kozelet/ontevekenykorok/page.tsx around lines 9 - 53,
The Organization type and the helper functions getContactLabel and
getOrganizationCardProps are duplicated across the refactored page components,
so move them into a shared utility module under organization-card and import
them from there. Keep the prop normalization logic centralized in that shared
helper, and make imageBasePath an optional parameter so the different pages can
pass their specific base path while reusing the same mapping logic. Update the
local usages in the page component to use the shared Organization and
getOrganizationCardProps symbols instead of redefining them.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/common/organization-card/sections/EventsSection.tsx`:
- Around line 51-67: The fallback key for non-linked items in EventsSection can
collide when multiple events share the same title. Update the events.map
rendering so the non-href branch uses a stable unique key that includes the
array index (or another unique field) instead of event.title alone, while
keeping the existing key behavior for linked events. Focus on the
EventsSection.tsx mapping logic around EventCard and the current key
expressions.

In `@src/components/common/organization-card/utils.tsx`:
- Around line 39-57: renderTextContent is treating every ReactNode array as an
array of strings, which breaks when content contains React elements. Update the
logic in renderTextContent to only call slice and parseFormattedText for string
items, and render non-string paragraphs directly or handle them in a separate
branch. Use the renderTextContent symbol and the paragraph mapping block to
locate the fix.

---

Nitpick comments:
In `@src/app/`(app)/[lang]/kozelet/ontevekenykorok/page.tsx:
- Around line 9-53: The Organization type and the helper functions
getContactLabel and getOrganizationCardProps are duplicated across the
refactored page components, so move them into a shared utility module under
organization-card and import them from there. Keep the prop normalization logic
centralized in that shared helper, and make imageBasePath an optional parameter
so the different pages can pass their specific base path while reusing the same
mapping logic. Update the local usages in the page component to use the shared
Organization and getOrganizationCardProps symbols instead of redefining them.

In `@src/lib/social-utils.tsx`:
- Around line 120-201: The label-matching logic for web/domain, email, and
X/Twitter is duplicated in getSocialIcon, getSocialPriority, and getSocialName,
so extract shared predicate helpers in social-utils.tsx and reuse them from all
three functions. Update the matching in getSocialPriority and getSocialName to
call the same helpers used by getSocialIcon so any future TLD or email rule
change only needs one edit and stays consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e15377b4-8877-4184-be33-763857bb9b2c

📥 Commits

Reviewing files that changed from the base of the PR and between 2cbebd5 and 4b203a6.

📒 Files selected for processing (16)
  • src/app/(app)/[lang]/kozelet/ontevekenykorok/page.tsx
  • src/app/(app)/[lang]/kozelet/szakkollegiumok/page.tsx
  • src/app/(app)/[lang]/kozelet/versenycsapatok/page.tsx
  • src/components/common/OrganizationCard.tsx
  • src/components/common/organization-card/sections/ContactsSection.tsx
  • src/components/common/organization-card/sections/EventsSection.tsx
  • src/components/common/organization-card/sections/GallerySection.tsx
  • src/components/common/organization-card/sections/Header.tsx
  • src/components/common/organization-card/sections/JoinCta.tsx
  • src/components/common/organization-card/sections/ListSections.tsx
  • src/components/common/organization-card/sections/Section.tsx
  • src/components/common/organization-card/sections/TextSections.tsx
  • src/components/common/organization-card/sections/index.ts
  • src/components/common/organization-card/types.ts
  • src/components/common/organization-card/utils.tsx
  • src/lib/social-utils.tsx

Comment on lines +51 to +67
{events.map((event) =>
event.href ? (
<a
key={`${event.title}-${event.href}`}
href={event.href}
target="_blank"
rel="noopener noreferrer"
className="block h-full"
>
<EventCard event={event} />
</a>
) : (
<div key={event.title}>
<EventCard event={event} />
</div>
),
)}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Non-linked event keys can collide on duplicate titles.

When event.href is absent, the key falls back to event.title alone. Two events with the same title (and no href) will produce duplicate React keys, causing reconciliation warnings and potential render bugs.

🔑 Proposed fix: include index in fallback key
         event.href ? (
           <a
             key={`${event.title}-${event.href}`}
             href={event.href}
             target="_blank"
             rel="noopener noreferrer"
             className="block h-full"
           >
             <EventCard event={event} />
           </a>
         ) : (
-          <div key={event.title}>
+          <div key={`${event.title}-${index}`}>
             <EventCard event={event} />
           </div>
         ),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/organization-card/sections/EventsSection.tsx` around
lines 51 - 67, The fallback key for non-linked items in EventsSection can
collide when multiple events share the same title. Update the events.map
rendering so the non-href branch uses a stable unique key that includes the
array index (or another unique field) instead of event.title alone, while
keeping the existing key behavior for linked events. Focus on the
EventsSection.tsx mapping logic around EventCard and the current key
expressions.

Comment on lines +39 to +57
export function renderTextContent(content: TextContent) {
if (Array.isArray(content)) {
return (
<div className="space-y-4">
{content.map((paragraph, index) => (
<p key={`${paragraph.slice(0, 28)}-${index}`}>
{parseFormattedText(paragraph)}
</p>
))}
</div>
);
}

if (typeof content === "string") {
return <p>{parseFormattedText(content)}</p>;
}

return content;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

renderTextContent crashes on ReactNode arrays.

TextContent includes ReactNode, which itself can be an array (e.g., [<span>…</span>]). Array.isArray(content) matches such arrays, then paragraph.slice(0, 28) throws because ReactElement has no .slice method. parseFormattedText(paragraph) also expects a string and would break on non-string elements.

Guard each paragraph before treating it as a string:

🐛 Proposed fix
 export function renderTextContent(content: TextContent) {
-  if (Array.isArray(content)) {
+  if (Array.isArray(content) && content.every((p) => typeof p === "string")) {
     return (
       <div className="space-y-4">
         {content.map((paragraph, index) => (
           <p key={`${paragraph.slice(0, 28)}-${index}`}>
             {parseFormattedText(paragraph)}
           </p>
         ))}
       </div>
     );
   }

Alternatively, if you want to support mixed arrays, handle non-string paragraphs separately:

   if (Array.isArray(content)) {
     return (
       <div className="space-y-4">
         {content.map((paragraph, index) =>
-          <p key={`${paragraph.slice(0, 28)}-${index}`}>
-            {parseFormattedText(paragraph)}
-          </p>
+          typeof paragraph === "string" ? (
+            <p key={`${paragraph.slice(0, 28)}-${index}`}>
+              {parseFormattedText(paragraph)}
+            </p>
+          ) : (
+            <p key={index}>{paragraph}</p>
+          )
         )}
       </div>
     );
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export function renderTextContent(content: TextContent) {
if (Array.isArray(content)) {
return (
<div className="space-y-4">
{content.map((paragraph, index) => (
<p key={`${paragraph.slice(0, 28)}-${index}`}>
{parseFormattedText(paragraph)}
</p>
))}
</div>
);
}
if (typeof content === "string") {
return <p>{parseFormattedText(content)}</p>;
}
return content;
}
export function renderTextContent(content: TextContent) {
if (Array.isArray(content) && content.every((p) => typeof p === "string")) {
return (
<div className="space-y-4">
{content.map((paragraph, index) => (
<p key={`${paragraph.slice(0, 28)}-${index}`}>
{parseFormattedText(paragraph)}
</p>
))}
</div>
);
}
if (typeof content === "string") {
return <p>{parseFormattedText(content)}</p>;
}
return content;
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/components/common/organization-card/utils.tsx` around lines 39 - 57,
renderTextContent is treating every ReactNode array as an array of strings,
which breaks when content contains React elements. Update the logic in
renderTextContent to only call slice and parseFormattedText for string items,
and render non-string paragraphs directly or handle them in a separate branch.
Use the renderTextContent symbol and the paragraph mapping block to locate the
fix.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant