diff --git a/backend/apps/api/rest/v0/project.py b/backend/apps/api/rest/v0/project.py index 7b26c3315a..6ebfe7fdce 100644 --- a/backend/apps/api/rest/v0/project.py +++ b/backend/apps/api/rest/v0/project.py @@ -15,6 +15,7 @@ from apps.api.rest.v0.structured_search import FieldConfig, apply_structured_search from apps.owasp.models.enums.project import ProjectLevel, ProjectType from apps.owasp.models.project import Project as ProjectModel +from apps.owasp.models.sponsor import Sponsor as SponsorModel PROJECT_SEARCH_FIELDS: dict[str, FieldConfig] = { "name": { @@ -55,6 +56,7 @@ class ProjectDetail(ProjectBase): description: str leaders: list[Leader] + sponsors: list["ProjectSponsor"] = [] @staticmethod def resolve_leaders(obj): @@ -64,6 +66,21 @@ def resolve_leaders(obj): for leader in obj.entity_leaders ] + @staticmethod + def resolve_sponsors(obj): + """Resolve sponsors linked to this project.""" + return [ + ProjectSponsor( + key=sponsor.key, + name=sponsor.name, + sponsor_type=sponsor.sponsor_type, + image_url=sponsor.image_url, + url=sponsor.url, + description=sponsor.description, + ) + for sponsor in obj.sponsors.filter(status=SponsorModel.Status.ACTIVE) + ] + class ProjectError(Schema): """Project error schema.""" @@ -71,6 +88,17 @@ class ProjectError(Schema): message: str +class ProjectSponsor(Schema): + """Schema for project sponsors.""" + + image_url: str + key: str + name: str + sponsor_type: str + url: str + description: str = "" + + class ProjectFilter(FilterSchema): """Filter for Project.""" diff --git a/backend/apps/api/rest/v0/sponsor.py b/backend/apps/api/rest/v0/sponsor.py index 4641e7639c..25a7f1ccb5 100644 --- a/backend/apps/api/rest/v0/sponsor.py +++ b/backend/apps/api/rest/v0/sponsor.py @@ -1,8 +1,11 @@ """Sponsor API.""" +import logging from http import HTTPStatus from typing import Literal +from django.core.exceptions import ValidationError +from django.db import IntegrityError from django.http import HttpRequest from ninja import Field, FilterSchema, Path, Query, Schema from ninja.decorators import decorate_view @@ -11,8 +14,11 @@ from apps.api.decorators.cache import cache_response from apps.api.rest.v0.common import ValidationErrorSchema +from apps.common.utils import slugify from apps.owasp.models.sponsor import Sponsor as SponsorModel +logger = logging.getLogger(__name__) + router = RouterPaginated(tags=["Sponsors"]) @@ -45,6 +51,23 @@ class SponsorError(Schema): message: str +class SponsorApplicationRequest(Schema): + """Schema for sponsor application request.""" + + name: str = Field(..., description="Organization name") + contact_email: str = Field(..., description="Contact email") + website: str | None = Field(None, description="Organization website (optional)") + sponsorship_interest: str = Field(..., description="Message about sponsorship interest") + + +class SponsorApplicationResponse(Schema): + """Schema for sponsor application response.""" + + id: int + name: str + status: str + + class SponsorFilter(FilterSchema): """Filter for Sponsor.""" @@ -105,3 +128,71 @@ def get_sponsor( return sponsor return Response({"message": "Sponsor not found"}, status=HTTPStatus.NOT_FOUND) + + +@router.post( + "/applications/", + description="Submit a sponsor application.", + operation_id="create_sponsor_application", + response={ + HTTPStatus.BAD_REQUEST: SponsorError, + HTTPStatus.CREATED: SponsorApplicationResponse, + }, + summary="Create sponsor application", +) +def create_sponsor_application( + request: HttpRequest, + payload: SponsorApplicationRequest, +) -> Response: + """Create a sponsor application.""" + try: + name = payload.name.strip() + contact_email = payload.contact_email.strip() + sponsorship_interest = payload.sponsorship_interest.strip() + website = (payload.website or "").strip() + + if not name or not contact_email or not sponsorship_interest: + return Response( + {"message": "Name, contact email, and sponsorship interest are required"}, + status=HTTPStatus.BAD_REQUEST, + ) + + key = slugify(name) + if not key: + return Response( + {"message": "Organization name is invalid"}, + status=HTTPStatus.BAD_REQUEST, + ) + + if SponsorModel.objects.filter(key=key).exists(): + return Response( + {"message": "A sponsor with this organization name already exists"}, + status=HTTPStatus.BAD_REQUEST, + ) + + sponsor = SponsorModel( + name=name, + key=key, + contact_email=contact_email, + url=website, + description=sponsorship_interest, + status=SponsorModel.Status.DRAFT, + sort_name=name, + ) + sponsor.full_clean() + sponsor.save() + + return Response( + SponsorApplicationResponse( + id=sponsor.id, + name=sponsor.name, + status=sponsor.status, + ), + status=HTTPStatus.CREATED, + ) + except (ValueError, ValidationError, IntegrityError) as e: + logger.warning("Error creating sponsor application: %s", e) + return Response( + {"message": "Error creating sponsor application"}, + status=HTTPStatus.BAD_REQUEST, + ) diff --git a/backend/apps/owasp/admin/sponsor.py b/backend/apps/owasp/admin/sponsor.py index c124b7cd67..801f0e2401 100644 --- a/backend/apps/owasp/admin/sponsor.py +++ b/backend/apps/owasp/admin/sponsor.py @@ -14,18 +14,25 @@ class SponsorAdmin(admin.ModelAdmin, StandardOwaspAdminMixin): "name", "sort_name", "sponsor_type", + "status", "is_member", "member_type", + "chapter", + "project", ) search_fields = ( "name", "sort_name", "description", + "contact_email", ) list_filter = ( "sponsor_type", + "status", "is_member", "member_type", + "chapter", + "project", ) fieldsets = ( ( @@ -35,6 +42,7 @@ class SponsorAdmin(admin.ModelAdmin, StandardOwaspAdminMixin): "name", "sort_name", "description", + "contact_email", ) }, ), @@ -55,10 +63,25 @@ class SponsorAdmin(admin.ModelAdmin, StandardOwaspAdminMixin): "is_member", "member_type", "sponsor_type", + "status", ) }, ), + ( + "Entity Associations", + { + "fields": ( + "chapter", + "project", + ), + "description": ( + "Optional: Link this sponsor to a specific chapter or project. " + "Leave blank for global sponsors." + ), + }, + ), ) + readonly_fields = ("nest_created_at", "nest_updated_at") admin.site.register(Sponsor, SponsorAdmin) diff --git a/backend/apps/owasp/api/internal/mutations/__init__.py b/backend/apps/owasp/api/internal/mutations/__init__.py new file mode 100644 index 0000000000..edf9a75e26 --- /dev/null +++ b/backend/apps/owasp/api/internal/mutations/__init__.py @@ -0,0 +1,7 @@ +"""OWASP GraphQL mutations.""" + +from .sponsor import SponsorMutations + + +class OwaspMutations(SponsorMutations): + """OWASP mutations.""" diff --git a/backend/apps/owasp/api/internal/mutations/sponsor.py b/backend/apps/owasp/api/internal/mutations/sponsor.py new file mode 100644 index 0000000000..7baf8e00e6 --- /dev/null +++ b/backend/apps/owasp/api/internal/mutations/sponsor.py @@ -0,0 +1,124 @@ +"""OWASP sponsors GraphQL mutations.""" + +import logging + +import strawberry +from django.core.exceptions import ValidationError +from django.db.utils import IntegrityError + +from apps.common.utils import slugify +from apps.owasp.api.internal.nodes.sponsor import SponsorNode +from apps.owasp.models.sponsor import Sponsor + +logger = logging.getLogger(__name__) + + +@strawberry.type +class CreateSponsorApplicationResult: + """Result of creating a sponsor application.""" + + ok: bool + sponsor: SponsorNode | None = None + code: str | None = None + message: str | None = None + + +@strawberry.type +class SponsorMutations: + """Sponsor mutations.""" + + @strawberry.mutation + def create_sponsor_application( + self, + name: str, + contact_email: str, + sponsorship_interest: str, + website: str | None = None, + ) -> CreateSponsorApplicationResult: + """Create a sponsor application. + + Args: + name: Organization name + contact_email: Contact email address + sponsorship_interest: Message about sponsorship interest + website: Organization website (optional) + + Returns: + CreateSponsorApplicationResult with sponsor application status + + """ + if not name or not name.strip(): + return CreateSponsorApplicationResult( + ok=False, + code="INVALID_NAME", + message="Organization name is required", + ) + + if not contact_email or not contact_email.strip(): + return CreateSponsorApplicationResult( + ok=False, + code="INVALID_EMAIL", + message="Contact email is required", + ) + + if not sponsorship_interest or not sponsorship_interest.strip(): + return CreateSponsorApplicationResult( + ok=False, + code="INVALID_INTEREST", + message="Sponsorship interest message is required", + ) + + try: + name_clean = name.strip() + email_clean = contact_email.strip() + interest_clean = sponsorship_interest.strip() + url_clean = website.strip() if website else "" + key = slugify(name_clean) + + # Validate before get_or_create to avoid saving invalid sponsor + temp_sponsor = Sponsor( + name=name_clean, + contact_email=email_clean, + url=url_clean, + description=interest_clean, + status=Sponsor.Status.DRAFT, + sort_name=name_clean, + key=key, + ) + temp_sponsor.full_clean(validate_unique=False) + + sponsor, created = Sponsor.objects.get_or_create( + key=key, + defaults={ + "name": name_clean, + "contact_email": email_clean, + "url": url_clean, + "description": interest_clean, + "status": Sponsor.Status.DRAFT, + "sort_name": name_clean, + }, + ) + + if not created: + return CreateSponsorApplicationResult( + ok=False, + code="DUPLICATE", + message="A sponsor with this organization name already exists", + ) + + logger.info("Sponsor application created: %s - %s", sponsor.id, sponsor.name) + + return CreateSponsorApplicationResult( + ok=True, + sponsor=sponsor, + code="SUCCESS", + message="Sponsor application submitted successfully", + ) + + except (ValidationError, IntegrityError) as err: + logger.warning("Error creating sponsor application: %s", err) + return CreateSponsorApplicationResult( + ok=False, + code="ERROR", + message="Error submitting sponsor application", + ) diff --git a/backend/apps/owasp/api/internal/nodes/chapter.py b/backend/apps/owasp/api/internal/nodes/chapter.py index 5b56f524c5..e02ca3315c 100644 --- a/backend/apps/owasp/api/internal/nodes/chapter.py +++ b/backend/apps/owasp/api/internal/nodes/chapter.py @@ -5,7 +5,9 @@ from apps.core.utils.index import deep_camelize from apps.owasp.api.internal.nodes.common import GenericEntityNode +from apps.owasp.api.internal.nodes.sponsor import SponsorNode from apps.owasp.models.chapter import Chapter +from apps.owasp.models.sponsor import Sponsor @strawberry.type @@ -61,3 +63,8 @@ def key(self, root: Chapter) -> str: def suggested_location(self, root: Chapter) -> str | None: """Resolve suggested location.""" return root.idx_suggested_location + + @strawberry_django.field(prefetch_related=["sponsors"]) + def sponsors(self, root: Chapter) -> list[SponsorNode]: + """Resolve active sponsors for this chapter.""" + return root.sponsors.filter(status=Sponsor.Status.ACTIVE).order_by("name") diff --git a/backend/apps/owasp/api/internal/nodes/project.py b/backend/apps/owasp/api/internal/nodes/project.py index cc1faf6b4f..ee202f873a 100644 --- a/backend/apps/owasp/api/internal/nodes/project.py +++ b/backend/apps/owasp/api/internal/nodes/project.py @@ -15,7 +15,9 @@ from apps.owasp.api.internal.nodes.project_health_metrics import ( ProjectHealthMetricsNode, ) +from apps.owasp.api.internal.nodes.sponsor import SponsorNode from apps.owasp.models.project import Project +from apps.owasp.models.sponsor import Sponsor RECENT_ISSUES_LIMIT = 5 RECENT_RELEASES_LIMIT = 5 @@ -131,6 +133,11 @@ def repositories_count(self, root: Project) -> int: """Resolve repositories count.""" return root.idx_repositories_count + @strawberry_django.field(prefetch_related=["sponsors"]) + def sponsors(self, root: Project) -> list[SponsorNode]: + """Resolve active sponsors for this project.""" + return root.sponsors.filter(status=Sponsor.Status.ACTIVE).order_by("name") + @strawberry_django.field def topics(self, root: Project) -> list[str]: """Resolve topics.""" diff --git a/backend/apps/owasp/api/internal/nodes/sponsor.py b/backend/apps/owasp/api/internal/nodes/sponsor.py index c66cb269b3..c496006c87 100644 --- a/backend/apps/owasp/api/internal/nodes/sponsor.py +++ b/backend/apps/owasp/api/internal/nodes/sponsor.py @@ -9,9 +9,13 @@ @strawberry_django.type( Sponsor, fields=[ + "description", + "id", "image_url", + "key", "name", "sponsor_type", + "status", "url", ], ) diff --git a/backend/apps/owasp/api/internal/queries/sponsor.py b/backend/apps/owasp/api/internal/queries/sponsor.py index 80014e529f..19374b90a8 100644 --- a/backend/apps/owasp/api/internal/queries/sponsor.py +++ b/backend/apps/owasp/api/internal/queries/sponsor.py @@ -15,7 +15,7 @@ class SponsorQuery: def sponsors(self) -> list[SponsorNode]: """Resolve sponsors.""" return sorted( - Sponsor.objects.all(), + Sponsor.objects.filter(status=Sponsor.Status.ACTIVE), key=lambda x: { Sponsor.SponsorType.DIAMOND: 1, Sponsor.SponsorType.PLATINUM: 2, diff --git a/backend/apps/owasp/migrations/0073_sponsor_chapter_sponsor_contact_email_and_more.py b/backend/apps/owasp/migrations/0073_sponsor_chapter_sponsor_contact_email_and_more.py new file mode 100644 index 0000000000..81bd5a1552 --- /dev/null +++ b/backend/apps/owasp/migrations/0073_sponsor_chapter_sponsor_contact_email_and_more.py @@ -0,0 +1,52 @@ +# Generated by Django 6.0.3 on 2026-03-20 06:53 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0072_project_project_name_gin_idx_and_more"), + ] + + operations = [ + migrations.AddField( + model_name="sponsor", + name="chapter", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="sponsors", + to="owasp.chapter", + verbose_name="Associated Chapter", + ), + ), + migrations.AddField( + model_name="sponsor", + name="contact_email", + field=models.EmailField(blank=True, max_length=254, verbose_name="Contact Email"), + ), + migrations.AddField( + model_name="sponsor", + name="project", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="sponsors", + to="owasp.project", + verbose_name="Associated Project", + ), + ), + migrations.AddField( + model_name="sponsor", + name="status", + field=models.CharField( + choices=[("draft", "Draft"), ("active", "Active"), ("archived", "Archived")], + default="active", + max_length=20, + verbose_name="Status", + ), + ), + ] diff --git a/backend/apps/owasp/models/sponsor.py b/backend/apps/owasp/models/sponsor.py index d7d0e38158..b1b4ae1106 100644 --- a/backend/apps/owasp/models/sponsor.py +++ b/backend/apps/owasp/models/sponsor.py @@ -37,6 +37,13 @@ class MemberType(models.TextChoices): GOLD = "Gold" SILVER = "Silver" + class Status(models.TextChoices): + """Sponsorship status choices.""" + + DRAFT = "draft" + ACTIVE = "active" + ARCHIVED = "archived" + # Basic information description = models.TextField(verbose_name="Description", blank=True) key = models.CharField(verbose_name="Key", max_length=100, unique=True) @@ -63,6 +70,32 @@ class MemberType(models.TextChoices): choices=SponsorType.choices, default=SponsorType.NOT_SPONSOR, ) + status = models.CharField( + verbose_name="Status", + max_length=20, + choices=Status.choices, + default=Status.ACTIVE, + ) + + contact_email = models.EmailField(verbose_name="Contact Email", blank=True) + + # Entity associations (optional) + chapter = models.ForeignKey( + "owasp.Chapter", + verbose_name="Associated Chapter", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="sponsors", + ) + project = models.ForeignKey( + "owasp.Project", + verbose_name="Associated Project", + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name="sponsors", + ) def __str__(self) -> str: """Sponsor human readable representation.""" @@ -78,6 +111,11 @@ def readable_sponsor_type(self) -> str: """Get human-readable sponsor type.""" return self.SponsorType(self.sponsor_type).label + @property + def readable_status(self) -> str: + """Get human-readable status.""" + return self.Status(self.status).label + @staticmethod def bulk_save( # type: ignore[override] sponsors: list[Sponsor], diff --git a/backend/settings/graphql.py b/backend/settings/graphql.py index 4f7fd8691a..be41a1bded 100644 --- a/backend/settings/graphql.py +++ b/backend/settings/graphql.py @@ -18,6 +18,7 @@ ProgramQuery, ) from apps.nest.api.internal.mutations import NestMutations +from apps.owasp.api.internal.mutations import OwaspMutations from apps.owasp.api.internal.queries import OwaspQuery @@ -26,6 +27,7 @@ class Mutation( ApiMutations, ModuleMutation, NestMutations, + OwaspMutations, ProgramMutation, ): """Schema mutations.""" diff --git a/docker-compose/local/compose.yaml b/docker-compose/local/compose.yaml index e72fc74855..01464c52d3 100644 --- a/docker-compose/local/compose.yaml +++ b/docker-compose/local/compose.yaml @@ -67,7 +67,7 @@ services: networks: - nest-network volumes: - - db-data:/var/lib/postgresql/data + - db-data-issue-4259:/var/lib/postgresql/data docs: container_name: nest-docs @@ -149,7 +149,7 @@ networks: volumes: backend-venv: cache-data: - db-data: + db-data-issue-4259: docs-venv: frontend-next: frontend-node-modules: diff --git a/frontend/src/app/chapters/[chapterKey]/page.tsx b/frontend/src/app/chapters/[chapterKey]/page.tsx index 663ea1311a..d791380844 100644 --- a/frontend/src/app/chapters/[chapterKey]/page.tsx +++ b/frontend/src/app/chapters/[chapterKey]/page.tsx @@ -10,6 +10,7 @@ import { getContributionStats } from 'utils/contributionDataUtils' import { formatDate, getDateRange } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' +import type { SponsorInfo } from 'components/SponsorBadge' export default function ChapterDetailsPage() { const { chapterKey } = useParams<{ chapterKey: string }>() @@ -25,6 +26,25 @@ export default function ChapterDetailsPage() { const chapter = data?.chapter const topContributors = data?.topContributors + const sponsors: SponsorInfo[] = (chapter?.sponsors || []).map( + (sponsor: { + key: string + name: string + sponsorType: string + imageUrl: string + url: string + description?: string + status?: string + }) => ({ + key: sponsor.key, + name: sponsor.name, + sponsorType: sponsor.sponsorType, + imageUrl: sponsor.imageUrl, + url: sponsor.url, + description: sponsor.description || '', + }) + ) + useEffect(() => { if (graphQLRequestError) { handleAppError(graphQLRequestError) @@ -87,6 +107,7 @@ export default function ChapterDetailsPage() { geolocationData={[chapter as unknown as Chapter]} isActive={chapter.isActive} socialLinks={chapter.relatedUrls} + sponsors={sponsors} startDate={startDate} summary={chapter.summary} title={chapter.name} diff --git a/frontend/src/app/projects/[projectKey]/page.tsx b/frontend/src/app/projects/[projectKey]/page.tsx index 456b060f16..3d5c599cbf 100644 --- a/frontend/src/app/projects/[projectKey]/page.tsx +++ b/frontend/src/app/projects/[projectKey]/page.tsx @@ -19,6 +19,7 @@ import { getContributionStats } from 'utils/contributionDataUtils' import { formatDate, getDateRange } from 'utils/dateFormatter' import DetailsCard from 'components/CardDetailsPage' import LoadingSpinner from 'components/LoadingSpinner' +import type { SponsorInfo } from 'components/SponsorBadge' const ProjectDetailsPage = () => { const { projectKey } = useParams<{ projectKey: string }>() @@ -33,6 +34,25 @@ const ProjectDetailsPage = () => { const project = data?.project const topContributors = data?.topContributors || [] + const sponsors: SponsorInfo[] = (project?.sponsors || []).map( + (sponsor: { + key: string + name: string + sponsorType: string + imageUrl: string + url: string + description?: string + status?: string + }) => ({ + key: sponsor.key, + name: sponsor.name, + sponsorType: sponsor.sponsorType, + imageUrl: sponsor.imageUrl, + url: sponsor.url, + description: sponsor.description || '', + }) + ) + useEffect(() => { if (graphQLRequestError) { handleAppError(graphQLRequestError) @@ -123,6 +143,7 @@ const ProjectDetailsPage = () => { recentMilestones={project.recentMilestones as unknown as Milestone[]} recentReleases={project.recentReleases as unknown as Release[]} repositories={project.repositories as unknown as RepositoryCardProps[]} + sponsors={sponsors} startDate={startDate} stats={projectStats} summary={project.summary} diff --git a/frontend/src/app/sponsors/apply/page.tsx b/frontend/src/app/sponsors/apply/page.tsx new file mode 100644 index 0000000000..7ed8fabb7e --- /dev/null +++ b/frontend/src/app/sponsors/apply/page.tsx @@ -0,0 +1,237 @@ +'use client' + +import { useMutation } from '@apollo/client/react' +import { addToast } from '@heroui/toast' +import type React from 'react' +import { useState } from 'react' +import { FaCheck, FaExclamation } from 'react-icons/fa6' +import { CREATE_SPONSOR_APPLICATION } from 'server/mutations/sponsorMutations' +import { FormButtons } from 'components/forms/shared/FormButtons' +import { FormContainer } from 'components/forms/shared/FormContainer' +import { FormTextarea } from 'components/forms/shared/FormTextarea' +import { FormTextInput } from 'components/forms/shared/FormTextInput' +import PageLayout from 'components/PageLayout' + +type FormStatus = 'idle' | 'loading' | 'success' | 'error' + +// RFC-compliant email regex: explicit character classes, no backtracking ambiguity, linear complexity +const EMAIL_REGEX = + /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/ + +interface FormData { + name: string + contactEmail: string + website: string + sponsorshipInterest: string +} + +const SponsorApplyPage = () => { + const [formData, setFormData] = useState({ + name: '', + contactEmail: '', + website: '', + sponsorshipInterest: '', + }) + + const [status, setStatus] = useState('idle') + const [errorMessage, setErrorMessage] = useState('') + const [touched, setTouched] = useState<{ [K in keyof FormData]?: boolean }>({}) + + const [createSponsorApplication, { loading: mutationLoading }] = useMutation( + CREATE_SPONSOR_APPLICATION + ) + + const isNameEmpty = (): boolean => formData.name.trim() === '' + const isEmailEmpty = (): boolean => formData.contactEmail.trim() === '' + const isValidEmail = (): boolean => EMAIL_REGEX.test(formData.contactEmail) + const isInvalidEmail = (): boolean => !isValidEmail() + const isInterestEmpty = (): boolean => formData.sponsorshipInterest.trim() === '' + const getNameError = (): string | undefined => { + if (!touched.name) return undefined + if (isNameEmpty()) return 'Organization name is required' + return undefined + } + + const getEmailError = (): string | undefined => { + if (!touched.contactEmail) return undefined + if (isEmailEmpty()) return 'Contact email is required' + if (isInvalidEmail()) return 'Please enter a valid email address' + return undefined + } + + const getInterestError = (): string | undefined => { + if (!touched.sponsorshipInterest) return undefined + if (isInterestEmpty()) return 'Please tell us about your sponsorship interest' + return undefined + } + + const handleInputChange = (name: keyof FormData, value: string) => { + setFormData((prev) => ({ + ...prev, + [name]: value, + })) + setTouched((prev) => ({ ...prev, [name]: true })) + } + + const validateForm = (): boolean => { + if (isNameEmpty()) { + setErrorMessage('Organization name is required') + return false + } + if (isEmailEmpty()) { + setErrorMessage('Contact email is required') + return false + } + if (isInvalidEmail()) { + setErrorMessage('Please enter a valid email address') + return false + } + if (isInterestEmpty()) { + setErrorMessage('Please tell us about your sponsorship interest') + return false + } + setErrorMessage('') + return true + } + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setErrorMessage('') + + if (!validateForm()) { + setStatus('error') + return + } + + setStatus('loading') + + try { + const { data } = await createSponsorApplication({ + variables: { + name: formData.name.trim(), + contactEmail: formData.contactEmail.trim(), + website: formData.website.trim() || null, + sponsorshipInterest: formData.sponsorshipInterest.trim(), + }, + }) + + if (data?.createSponsorApplication?.ok) { + setStatus('success') + setFormData({ + name: '', + contactEmail: '', + website: '', + sponsorshipInterest: '', + }) + addToast({ + title: 'Application Submitted', + description: 'Thank you for your interest. We will review your application shortly.', + color: 'success', + variant: 'solid', + timeout: 4000, + }) + } else { + setStatus('error') + setErrorMessage(data?.createSponsorApplication?.message || 'Failed to submit application') + } + } catch (error) { + setStatus('error') + setErrorMessage( + error instanceof Error ? error.message : 'An error occurred while submitting the form' + ) + } + } + + return ( + +
+
+

+ Become a Sponsor +

+

+ We appreciate your interest in supporting OWASP Nest. Please fill out this form to apply + for a sponsorship opportunity. +

+ {status === 'success' ? ( +
+
+
+ +
+
+

+ Application Submitted Successfully! +

+

+ Thank you for your interest in sponsoring OWASP Nest. We have received your + application and will review it shortly. Our team will contact you at the email + address provided. +

+
+ ) : ( + + {status === 'error' && errorMessage && ( +
+
+ +

{errorMessage}

+
+
+ )} + handleInputChange('name', v)} + error={getNameError()} + touched={touched.name} + required + placeholder="Your organization name" + /> + handleInputChange('contactEmail', v)} + error={getEmailError()} + touched={touched.contactEmail} + required + placeholder="your.email@example.com" + /> + handleInputChange('website', v)} + error={undefined} + touched={touched.website} + placeholder="https://example.com" + /> + handleInputChange('sponsorshipInterest', e.target.value)} + error={getInterestError()} + touched={touched.sponsorshipInterest} + required + placeholder="Tell us about your interest in sponsoring OWASP Nest..." + rows={5} + /> + +
+ )} +
+
+
+ ) +} + +export default SponsorApplyPage diff --git a/frontend/src/app/sponsors/page.tsx b/frontend/src/app/sponsors/page.tsx new file mode 100644 index 0000000000..d393756c3d --- /dev/null +++ b/frontend/src/app/sponsors/page.tsx @@ -0,0 +1,370 @@ +'use client' + +import { useQuery } from '@apollo/client/react' +import Image from 'next/image' +import Link from 'next/link' +import { useMemo } from 'react' +import { GetSponsorsDocument } from 'types/__generated__/sponsorQueries.generated' +import type { Sponsor } from 'types/home' +import LoadingSpinner from 'components/LoadingSpinner' +import PageLayout from 'components/PageLayout' + +const SponsorsPage = () => { + const getSafeHttpUrl = (url: string | undefined | null): string => { + if (!url) return '#' + try { + const parsed = new URL(url) + if (parsed.protocol === 'http:' || parsed.protocol === 'https:') { + return parsed.toString() + } + } catch { + // Invalid URL, return fallback + } + return '#' + } + + const isValidUrl = (url: string): boolean => url !== '#' + + const { data, loading, error } = useQuery(GetSponsorsDocument) + + const groupedSponsors = useMemo(() => { + const sponsors = (data?.sponsors ?? []).filter( + (sponsor) => sponsor.status?.toLowerCase() === 'active' + ) + const groups: Record = { + diamond: [], + platinum: [], + gold: [], + silver: [], + supporter: [], + } + + sponsors.forEach((sponsor: Sponsor) => { + const sponsorTypeKey = sponsor.sponsorType.toLowerCase() + if (sponsorTypeKey in groups) { + groups[sponsorTypeKey].push(sponsor) + } + }) + + return groups + }, [data?.sponsors]) + + if (loading) { + return ( + +
+ +
+
+ ) + } + + if (error) { + return ( + +
+
+

Error Loading Sponsors

+

Please try again later.

+
+
+
+ ) + } + + const logoWrapperBase = + 'flex items-center justify-center rounded-lg px-3 py-2 border bg-white dark:bg-[#e8e8e8]/90 border-gray-200 dark:border-transparent' + + const renderDiamondCard = (sponsor: Sponsor) => { + const cardContent = ( + <> + {sponsor.imageUrl && ( +
+ {sponsor.name} +
+ )} + +
+

+ {sponsor.name} +

+ {sponsor.description && ( +

+ {sponsor.description} +

+ )} +
+ + ) + + const baseClassName = + 'group relative mb-6 flex flex-row items-center justify-start gap-8 rounded-lg bg-gradient-to-r from-white to-blue-50 p-7 transition-all duration-300 ease-in-out hover:scale-[1.01] hover:shadow-lg dark:from-[#2d3139] dark:to-[#3a4250] dark:hover:shadow-xl' + const safeUrl = getSafeHttpUrl(sponsor.url) + const interactiveClassName = isValidUrl(safeUrl) + ? `cursor-pointer ${baseClassName}` + : baseClassName + + if (isValidUrl(safeUrl)) { + return ( + + {cardContent} + + ) + } + + return ( +
+ {cardContent} +
+ ) + } + + const renderSponsorCard = (sponsor: Sponsor, size: 'large' | 'medium' | 'small') => { + const getTextSizeClass = (): string => { + if (size === 'large') return 'text-base' + if (size === 'medium') return 'text-sm' + return 'text-xs' + } + + const cardClasses = { + large: 'p-5 gap-4', + medium: 'p-4 gap-3', + small: 'p-3 gap-2', + } + + const logoSizeClasses = { + large: 'h-24 w-48', + medium: 'h-20 w-40', + small: 'h-14 w-32', + } + + const logoDimensions = { + large: { height: 96, width: 192 }, + medium: { height: 80, width: 160 }, + small: { height: 56, width: 128 }, + } + + const cardContent = ( +
+ {sponsor.imageUrl && ( +
+ {sponsor.name} +
+ )} +

+ {sponsor.name} +

+ {size === 'large' && sponsor.description && ( +

+ {sponsor.description} +

+ )} +
+ ) + + const baseClassName = + 'group relative flex flex-col items-center justify-center rounded-lg border border-gray-200 bg-white transition-all duration-300 ease-in-out hover:scale-[1.02] hover:shadow-lg dark:border-gray-700 dark:bg-[#2d3139] dark:hover:border-gray-500 dark:hover:shadow-xl' + const safeUrl = getSafeHttpUrl(sponsor.url) + const interactiveClassName = isValidUrl(safeUrl) + ? `cursor-pointer ${baseClassName}` + : baseClassName + + if (isValidUrl(safeUrl)) { + return ( + + {cardContent} + + ) + } + + return ( +
+ {cardContent} +
+ ) + } + + const hasDiamond = groupedSponsors.diamond.length > 0 + const hasPlatinum = groupedSponsors.platinum.length > 0 + const hasGold = groupedSponsors.gold.length > 0 + const hasSilver = groupedSponsors.silver.length > 0 + const hasSupporter = groupedSponsors.supporter.length > 0 + const hasAnySponsor = hasDiamond || hasPlatinum || hasGold || hasSilver || hasSupporter + const hasNoSponsors = !hasAnySponsor + + return ( + +
+
+
+

+ Sponsors +

+

+ Supporting OWASP Nest and its global community of contributors. +

+
+ + Apply to Sponsor + + + Direct Donation + +
+
+ + {hasNoSponsors ? ( +
+

No active sponsors at this time.

+
+ ) : ( + <> + {hasDiamond && ( +
+
+
+

+ Diamond Sponsors +

+
+
+
+ {groupedSponsors.diamond.map((sponsor) => renderDiamondCard(sponsor))} +
+
+ )} + + {hasPlatinum && ( +
+
+
+

+ Platinum Sponsors +

+
+
+
+ {groupedSponsors.platinum.map((sponsor) => renderSponsorCard(sponsor, 'large'))} +
+
+ )} + + {hasGold && ( +
+
+
+

+ Gold Sponsors +

+
+
+
+ {groupedSponsors.gold.map((sponsor) => renderSponsorCard(sponsor, 'medium'))} +
+
+ )} + + {hasSilver && ( +
+
+
+

+ Silver Sponsors +

+
+
+
+ {groupedSponsors.silver.map((sponsor) => renderSponsorCard(sponsor, 'small'))} +
+
+ )} + + {hasSupporter && ( +
+
+
+

+ Supporters +

+
+
+
+ {groupedSponsors.supporter.map((sponsor) => + renderSponsorCard(sponsor, 'small') + )} +
+
+ )} + + )} + +
+

+ Interested in Sponsoring OWASP Nest? +

+

+ Help us support the global cybersecurity community and expand open-source security + initiatives. +

+
+ + Apply to Sponsor + + + Donate Now + +
+
+
+
+
+ ) +} + +export default SponsorsPage diff --git a/frontend/src/components/CardDetailsPage.tsx b/frontend/src/components/CardDetailsPage.tsx index 9c859d5162..5b885001d7 100644 --- a/frontend/src/components/CardDetailsPage.tsx +++ b/frontend/src/components/CardDetailsPage.tsx @@ -46,6 +46,7 @@ import RecentReleases from 'components/RecentReleases' import RepositoryCard from 'components/RepositoryCard' import SecondaryCard from 'components/SecondaryCard' import ShowMoreButton from 'components/ShowMoreButton' +import { SponsorList } from 'components/SponsorBadge' import SponsorCard from 'components/SponsorCard' import StatusBadge from 'components/StatusBadge' import ToggleableList from 'components/ToggleableList' @@ -112,6 +113,7 @@ const DetailsCard = ({ title, topContributors, topics, + sponsors, type, userSummary, }: DetailsCardProps) => { @@ -194,6 +196,14 @@ const DetailsCard = ({ )} {userSummary && {userSummary}} + {sponsors && sponsors.length > 0 && ['project', 'chapter'].includes(type) && ( + } + > + + + )}
= { + diamond: { + label: 'Diamond Sponsor', + ring: 'ring-1 ring-cyan-400/40 dark:ring-cyan-400/25', + badge: 'bg-cyan-100 text-cyan-800 dark:bg-cyan-400/10 dark:text-cyan-300', + logoBg: 'bg-white dark:bg-white', + logoText: 'text-cyan-800 dark:text-cyan-800', + glow: 'shadow-[0_0_0_1px_rgba(34,211,238,0.1)] dark:shadow-[0_0_24px_rgba(34,211,238,0.07)]', + icon: '💎', + }, + platinum: { + label: 'Platinum Sponsor', + ring: 'ring-1 ring-slate-300/40 dark:ring-slate-400/30', + badge: 'bg-slate-100 text-slate-700 dark:bg-slate-700/50 dark:text-slate-200', + logoBg: 'bg-white dark:bg-white', + logoText: 'text-slate-700 dark:text-slate-700', + glow: 'shadow-[0_0_0_1px_rgba(148,163,184,0.15)] dark:shadow-[0_0_24px_rgba(148,163,184,0.06)]', + icon: '✦', + }, + gold: { + label: 'Gold Sponsor', + ring: 'ring-1 ring-amber-400/40 dark:ring-amber-400/25', + badge: 'bg-amber-100 text-amber-800 dark:bg-amber-400/10 dark:text-amber-300', + logoBg: 'bg-white dark:bg-white', + logoText: 'text-amber-800 dark:text-amber-800', + glow: 'shadow-[0_0_0_1px_rgba(251,191,36,0.1)] dark:shadow-[0_0_24px_rgba(251,191,36,0.07)]', + icon: '★', + }, + silver: { + label: 'Silver Sponsor', + ring: 'ring-1 ring-gray-300/40 dark:ring-gray-500/30', + badge: 'bg-gray-100 text-gray-700 dark:bg-gray-700/40 dark:text-gray-300', + logoBg: 'bg-white dark:bg-white', + logoText: 'text-gray-700 dark:text-gray-700', + glow: '', + icon: '◆', + }, + default: { + label: 'Sponsor', + ring: 'ring-1 ring-gray-200/40 dark:ring-gray-700/40', + badge: 'bg-gray-100 text-gray-600 dark:bg-gray-800/60 dark:text-gray-400', + logoBg: 'bg-white dark:bg-white', + logoText: 'text-gray-700 dark:text-gray-700', + glow: '', + icon: '◇', + }, +} + +const getTier = (sponsorType: string) => + TIER_CONFIG[sponsorType?.toLowerCase()] ?? TIER_CONFIG.default + +const getSafeExternalHref = (rawUrl: string): string => { + try { + const parsed = new URL(rawUrl) + if (parsed.protocol === 'http:' || parsed.protocol === 'https:') return parsed.toString() + } catch { + // Invalid URL, return fallback + } + return '#' +} + +interface SponsorBadgeProps { + sponsor: SponsorInfo + variant?: 'compact' | 'card' +} + +const SponsorBadge: React.FC = ({ sponsor, variant = 'compact' }) => { + const tier = getTier(sponsor.sponsorType) + const safeHref = getSafeExternalHref(sponsor.url) + + if (variant === 'card') { + return ( + +
+
+ {sponsor.imageUrl ? ( + {sponsor.name} + ) : ( +
+ {sponsor.name.charAt(0).toUpperCase()} +
+ )} +
+ + + {tier.icon} + {sponsor.sponsorType} + +
+ +
+

{sponsor.name}

+ {sponsor.description && ( +

+ {sponsor.description} +

+ )} +
+ +
+ Learn more + +
+ + ) + } + return ( + +
+ {sponsor.imageUrl ? ( + {sponsor.name} + ) : ( +
+
+ {sponsor.name + .split(' ') + .slice(0, 2) + .map((w) => w.charAt(0)) + .join('') + .toUpperCase()} +
+
+ {sponsor.name.split(' ')[0].slice(0, 8)} +
+
+ )} +
+ +

+ {sponsor.name} +

+ + + {tier.icon} + {sponsor.sponsorType} + + + ) +} + +interface SponsorListProps { + sponsors: SponsorInfo[] + variant?: 'compact' | 'card' + title?: string +} + +export const SponsorList: React.FC = ({ sponsors, variant = 'card', title }) => { + if (!sponsors || sponsors.length === 0) return null + + return ( +
+ {title && ( +
+ +

+ {title} +

+
+ )} +
+ {sponsors.map((sponsor) => ( +
+ +
+ ))} +
+
+ ) +} + +export default SponsorBadge diff --git a/frontend/src/server/mutations/sponsorMutations.ts b/frontend/src/server/mutations/sponsorMutations.ts new file mode 100644 index 0000000000..ce562eeb65 --- /dev/null +++ b/frontend/src/server/mutations/sponsorMutations.ts @@ -0,0 +1,29 @@ +import { gql } from '@apollo/client' + +export const CREATE_SPONSOR_APPLICATION = gql` + mutation CreateSponsorApplication( + $name: String! + $contactEmail: String! + $sponsorshipInterest: String! + $website: String + ) { + createSponsorApplication( + name: $name + contactEmail: $contactEmail + sponsorshipInterest: $sponsorshipInterest + website: $website + ) { + ok + code + message + sponsor { + id + description + name + sponsorType + status + url + } + } + } +` diff --git a/frontend/src/server/queries/chapterQueries.ts b/frontend/src/server/queries/chapterQueries.ts index 2978fd11d3..f55e1e5ca8 100644 --- a/frontend/src/server/queries/chapterQueries.ts +++ b/frontend/src/server/queries/chapterQueries.ts @@ -30,6 +30,15 @@ export const GET_CHAPTER_DATA = gql` summary updatedAt url + sponsors { + description + imageUrl + key + name + sponsorType + status + url + } } topContributors(chapter: $key) { id diff --git a/frontend/src/server/queries/projectQueries.ts b/frontend/src/server/queries/projectQueries.ts index 8d4c261519..20b0d9e724 100644 --- a/frontend/src/server/queries/projectQueries.ts +++ b/frontend/src/server/queries/projectQueries.ts @@ -86,6 +86,15 @@ export const GET_PROJECT_DATA = gql` url } repositoriesCount + sponsors { + description + imageUrl + key + name + sponsorType + status + url + } starsCount summary topics diff --git a/frontend/src/server/queries/sponsorQueries.ts b/frontend/src/server/queries/sponsorQueries.ts new file mode 100644 index 0000000000..d9f8d4a425 --- /dev/null +++ b/frontend/src/server/queries/sponsorQueries.ts @@ -0,0 +1,15 @@ +import { gql } from '@apollo/client' + +export const GET_SPONSORS = gql` + query GetSponsors { + sponsors { + id + description + imageUrl + name + sponsorType + status + url + } + } +` diff --git a/frontend/src/types/__generated__/chapterQueries.generated.ts b/frontend/src/types/__generated__/chapterQueries.generated.ts index 952c9e7a9b..0bc63f826a 100644 --- a/frontend/src/types/__generated__/chapterQueries.generated.ts +++ b/frontend/src/types/__generated__/chapterQueries.generated.ts @@ -6,7 +6,7 @@ export type GetChapterDataQueryVariables = Types.Exact<{ }>; -export type GetChapterDataQuery = { chapter: { __typename: 'ChapterNode', contributionData: any, contributionStats: any | null, id: string, isActive: boolean, key: string, name: string, region: string, relatedUrls: Array, suggestedLocation: string | null, summary: string, updatedAt: string, url: string, entityLeaders: Array<{ __typename: 'EntityMemberNode', id: string, description: string, memberName: string, member: { __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string } | null }>, geoLocation: { __typename: 'GeoLocationType', lat: number, lng: number } | null } | null, topContributors: Array<{ __typename: 'RepositoryContributorNode', id: string, avatarUrl: string, login: string, name: string }> }; +export type GetChapterDataQuery = { chapter: { __typename: 'ChapterNode', contributionData: any, contributionStats: any | null, id: string, isActive: boolean, key: string, name: string, region: string, relatedUrls: Array, suggestedLocation: string | null, summary: string, updatedAt: string, url: string, entityLeaders: Array<{ __typename: 'EntityMemberNode', id: string, description: string, memberName: string, member: { __typename: 'UserNode', id: string, login: string, name: string, avatarUrl: string } | null }>, geoLocation: { __typename: 'GeoLocationType', lat: number, lng: number } | null, sponsors: Array<{ __typename: 'SponsorNode', description: string, imageUrl: string, key: string, name: string, sponsorType: string, status: string, url: string }> } | null, topContributors: Array<{ __typename: 'RepositoryContributorNode', id: string, avatarUrl: string, login: string, name: string }> }; export type GetChapterMetadataQueryVariables = Types.Exact<{ key: Types.Scalars['String']['input']; @@ -21,6 +21,6 @@ export type GetChapterCountriesQueryVariables = Types.Exact<{ [key: string]: nev export type GetChapterCountriesQuery = { chapterCountries: Array }; -export const GetChapterDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChapterData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contributionData"}},{"kind":"Field","name":{"kind":"Name","value":"contributionStats"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"entityLeaders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"memberName"}},{"kind":"Field","name":{"kind":"Name","value":"member"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"geoLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lat"}},{"kind":"Field","name":{"kind":"Name","value":"lng"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"relatedUrls"}},{"kind":"Field","name":{"kind":"Name","value":"suggestedLocation"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"topContributors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"chapter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; +export const GetChapterDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChapterData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contributionData"}},{"kind":"Field","name":{"kind":"Name","value":"contributionStats"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"entityLeaders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"memberName"}},{"kind":"Field","name":{"kind":"Name","value":"member"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"geoLocation"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lat"}},{"kind":"Field","name":{"kind":"Name","value":"lng"}}]}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"region"}},{"kind":"Field","name":{"kind":"Name","value":"relatedUrls"}},{"kind":"Field","name":{"kind":"Name","value":"suggestedLocation"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"sponsors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sponsorType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"topContributors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"chapter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const GetChapterMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChapterMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapter"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}}]}}]}}]} as unknown as DocumentNode; export const GetChapterCountriesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetChapterCountries"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"chapterCountries"}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/types/__generated__/graphql.ts b/frontend/src/types/__generated__/graphql.ts index 0e0cb4ff2a..63e3e28fba 100644 --- a/frontend/src/types/__generated__/graphql.ts +++ b/frontend/src/types/__generated__/graphql.ts @@ -84,6 +84,7 @@ export type ChapterNode = Node & { postalCode: Scalars['String']['output']; region: Scalars['String']['output']; relatedUrls: Array; + sponsors: Array; suggestedLocation?: Maybe; summary: Scalars['String']['output']; tags: Scalars['JSON']['output']; @@ -146,6 +147,14 @@ export type CreateProgramInput = { tags?: Array; }; +export type CreateSponsorApplicationResult = { + __typename?: 'CreateSponsorApplicationResult'; + code?: Maybe; + message?: Maybe; + ok: Scalars['Boolean']['output']; + sponsor?: Maybe; +}; + export type EntityMemberNode = Node & { __typename?: 'EntityMemberNode'; description: Scalars['String']['output']; @@ -387,6 +396,7 @@ export type Mutation = { createApiKey: CreateApiKeyResult; createModule: ModuleNode; createProgram: ProgramNode; + createSponsorApplication: CreateSponsorApplicationResult; deleteModule: Scalars['String']['output']; githubAuth: GitHubAuthResult; logoutUser: LogoutResult; @@ -431,6 +441,14 @@ export type MutationCreateProgramArgs = { }; +export type MutationCreateSponsorApplicationArgs = { + contactEmail: Scalars['String']['input']; + name: Scalars['String']['input']; + sponsorshipInterest: Scalars['String']['input']; + website?: InputMaybe; +}; + + export type MutationDeleteModuleArgs = { moduleKey: Scalars['String']['input']; programKey: Scalars['String']['input']; @@ -672,6 +690,7 @@ export type ProjectNode = Node & { relatedUrls: Array; repositories: Array; repositoriesCount: Scalars['Int']['output']; + sponsors: Array; starsCount: Scalars['Int']['output']; summary: Scalars['String']['output']; topContributors: Array; @@ -1054,11 +1073,13 @@ export type SnapshotNode = Node & { export type SponsorNode = Node & { __typename?: 'SponsorNode'; - /** The Globally Unique ID of this object */ + description: Scalars['String']['output']; id: Scalars['ID']['output']; imageUrl: Scalars['String']['output']; + key: Scalars['String']['output']; name: Scalars['String']['output']; sponsorType: Scalars['String']['output']; + status: Scalars['String']['output']; url: Scalars['String']['output']; }; diff --git a/frontend/src/types/__generated__/projectQueries.generated.ts b/frontend/src/types/__generated__/projectQueries.generated.ts index 3ea7c92cd3..086d7b5ce1 100644 --- a/frontend/src/types/__generated__/projectQueries.generated.ts +++ b/frontend/src/types/__generated__/projectQueries.generated.ts @@ -6,7 +6,7 @@ export type GetProjectQueryVariables = Types.Exact<{ }>; -export type GetProjectQuery = { project: { __typename: 'ProjectNode', contributionData: any | null, contributionStats: any | null, contributorsCount: number, forksCount: number, id: string, isActive: boolean, issuesCount: number, key: string, languages: Array, leaders: Array, level: string, name: string, repositoriesCount: number, starsCount: number, summary: string, topics: Array, type: string, updatedAt: string, url: string, entityLeaders: Array<{ __typename: 'EntityMemberNode', description: string, id: string, memberName: string, member: { __typename: 'UserNode', avatarUrl: string, id: string, login: string, name: string } | null }>, healthMetricsList: Array<{ __typename: 'ProjectHealthMetricsNode', id: string, createdAt: any, forksCount: number, lastCommitDays: number, lastCommitDaysRequirement: number, lastReleaseDays: number, lastReleaseDaysRequirement: number, openIssuesCount: number, openPullRequestsCount: number, score: number | null, starsCount: number, unassignedIssuesCount: number, unansweredIssuesCount: number }>, recentIssues: Array<{ __typename: 'IssueNode', createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string, url: string } | null }>, recentReleases: Array<{ __typename: 'ReleaseNode', id: string, name: string, organizationName: string | null, publishedAt: any | null, repositoryName: string | null, tagName: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }>, repositories: Array<{ __typename: 'RepositoryNode', id: string, contributorsCount: number, forksCount: number, isArchived: boolean, key: string, name: string, openIssuesCount: number, starsCount: number, subscribersCount: number, url: string, organization: { __typename: 'OrganizationNode', login: string } | null }>, recentMilestones: Array<{ __typename: 'MilestoneNode', title: string, openIssuesCount: number, closedIssuesCount: number, repositoryName: string | null, organizationName: string | null, createdAt: any, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }>, recentPullRequests: Array<{ __typename: 'PullRequestNode', createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }> } | null, topContributors: Array<{ __typename: 'RepositoryContributorNode', id: string, avatarUrl: string, login: string, name: string }> }; +export type GetProjectQuery = { project: { __typename: 'ProjectNode', contributionData: any | null, contributionStats: any | null, contributorsCount: number, forksCount: number, id: string, isActive: boolean, issuesCount: number, key: string, languages: Array, leaders: Array, level: string, name: string, repositoriesCount: number, starsCount: number, summary: string, topics: Array, type: string, updatedAt: string, url: string, entityLeaders: Array<{ __typename: 'EntityMemberNode', description: string, id: string, memberName: string, member: { __typename: 'UserNode', avatarUrl: string, id: string, login: string, name: string } | null }>, healthMetricsList: Array<{ __typename: 'ProjectHealthMetricsNode', id: string, createdAt: any, forksCount: number, lastCommitDays: number, lastCommitDaysRequirement: number, lastReleaseDays: number, lastReleaseDaysRequirement: number, openIssuesCount: number, openPullRequestsCount: number, score: number | null, starsCount: number, unassignedIssuesCount: number, unansweredIssuesCount: number }>, recentIssues: Array<{ __typename: 'IssueNode', createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string, url: string } | null }>, recentReleases: Array<{ __typename: 'ReleaseNode', id: string, name: string, organizationName: string | null, publishedAt: any | null, repositoryName: string | null, tagName: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }>, repositories: Array<{ __typename: 'RepositoryNode', id: string, contributorsCount: number, forksCount: number, isArchived: boolean, key: string, name: string, openIssuesCount: number, starsCount: number, subscribersCount: number, url: string, organization: { __typename: 'OrganizationNode', login: string } | null }>, sponsors: Array<{ __typename: 'SponsorNode', description: string, imageUrl: string, key: string, name: string, sponsorType: string, status: string, url: string }>, recentMilestones: Array<{ __typename: 'MilestoneNode', title: string, openIssuesCount: number, closedIssuesCount: number, repositoryName: string | null, organizationName: string | null, createdAt: any, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }>, recentPullRequests: Array<{ __typename: 'PullRequestNode', createdAt: any, organizationName: string | null, repositoryName: string | null, title: string, url: string, author: { __typename: 'UserNode', id: string, avatarUrl: string, login: string, name: string } | null }> } | null, topContributors: Array<{ __typename: 'RepositoryContributorNode', id: string, avatarUrl: string, login: string, name: string }> }; export type GetProjectMetadataQueryVariables = Types.Exact<{ key: Types.Scalars['String']['input']; @@ -33,7 +33,7 @@ export type SearchProjectNamesQueryVariables = Types.Exact<{ export type SearchProjectNamesQuery = { searchProjects: Array<{ __typename: 'ProjectNode', id: string, name: string }> }; -export const GetProjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contributionData"}},{"kind":"Field","name":{"kind":"Name","value":"contributionStats"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"entityLeaders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"memberName"}},{"kind":"Field","name":{"kind":"Name","value":"member"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"issuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"languages"}},{"kind":"Field","name":{"kind":"Name","value":"leaders"}},{"kind":"Field","name":{"kind":"Name","value":"level"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"healthMetricsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"30"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"lastCommitDays"}},{"kind":"Field","name":{"kind":"Name","value":"lastCommitDaysRequirement"}},{"kind":"Field","name":{"kind":"Name","value":"lastReleaseDays"}},{"kind":"Field","name":{"kind":"Name","value":"lastReleaseDaysRequirement"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"openPullRequestsCount"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"unassignedIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"unansweredIssuesCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentIssues"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentReleases"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"publishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"tagName"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"repositories"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"isArchived"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"organization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"login"}}]}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"subscribersCount"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"repositoriesCount"}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}},{"kind":"Field","name":{"kind":"Name","value":"topics"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"recentMilestones"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"closedIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentPullRequests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"topContributors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"project"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; +export const GetProjectDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProject"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"contributionData"}},{"kind":"Field","name":{"kind":"Name","value":"contributionStats"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"entityLeaders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"memberName"}},{"kind":"Field","name":{"kind":"Name","value":"member"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"issuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"languages"}},{"kind":"Field","name":{"kind":"Name","value":"leaders"}},{"kind":"Field","name":{"kind":"Name","value":"level"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"healthMetricsList"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"30"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"lastCommitDays"}},{"kind":"Field","name":{"kind":"Name","value":"lastCommitDaysRequirement"}},{"kind":"Field","name":{"kind":"Name","value":"lastReleaseDays"}},{"kind":"Field","name":{"kind":"Name","value":"lastReleaseDaysRequirement"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"openPullRequestsCount"}},{"kind":"Field","name":{"kind":"Name","value":"score"}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"unassignedIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"unansweredIssuesCount"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentIssues"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentReleases"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"publishedAt"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"tagName"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"repositories"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"isArchived"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"organization"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"login"}}]}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"subscribersCount"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"repositoriesCount"}},{"kind":"Field","name":{"kind":"Name","value":"sponsors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"}},{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sponsorType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}},{"kind":"Field","name":{"kind":"Name","value":"topics"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"recentMilestones"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"5"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"openIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"closedIssuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}},{"kind":"Field","name":{"kind":"Name","value":"recentPullRequests"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"author"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"organizationName"}},{"kind":"Field","name":{"kind":"Name","value":"repositoryName"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"topContributors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"project"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const GetProjectMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetProjectMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"project"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"key"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"contributorsCount"}},{"kind":"Field","name":{"kind":"Name","value":"forksCount"}},{"kind":"Field","name":{"kind":"Name","value":"issuesCount"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"starsCount"}},{"kind":"Field","name":{"kind":"Name","value":"summary"}},{"kind":"Field","name":{"kind":"Name","value":"recentMilestones"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"IntValue","value":"25"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"body"}},{"kind":"Field","name":{"kind":"Name","value":"progress"}},{"kind":"Field","name":{"kind":"Name","value":"state"}}]}}]}}]}}]} as unknown as DocumentNode; export const GetTopContributorsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetTopContributors"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"excludedUsernames"}},"type":{"kind":"ListType","type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"hasFullName"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}},"defaultValue":{"kind":"BooleanValue","value":false}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"key"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"limit"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"Int"}},"defaultValue":{"kind":"IntValue","value":"20"}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"topContributors"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"excludedUsernames"},"value":{"kind":"Variable","name":{"kind":"Name","value":"excludedUsernames"}}},{"kind":"Argument","name":{"kind":"Name","value":"hasFullName"},"value":{"kind":"Variable","name":{"kind":"Name","value":"hasFullName"}}},{"kind":"Argument","name":{"kind":"Name","value":"limit"},"value":{"kind":"Variable","name":{"kind":"Name","value":"limit"}}},{"kind":"Argument","name":{"kind":"Name","value":"project"},"value":{"kind":"Variable","name":{"kind":"Name","value":"key"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"avatarUrl"}},{"kind":"Field","name":{"kind":"Name","value":"login"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; export const SearchProjectNamesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"SearchProjectNames"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"query"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"searchProjects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"query"},"value":{"kind":"Variable","name":{"kind":"Name","value":"query"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/types/__generated__/sponsorMutations.generated.ts b/frontend/src/types/__generated__/sponsorMutations.generated.ts new file mode 100644 index 0000000000..f5e8945061 --- /dev/null +++ b/frontend/src/types/__generated__/sponsorMutations.generated.ts @@ -0,0 +1,15 @@ +import * as Types from './graphql'; + +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type CreateSponsorApplicationMutationVariables = Types.Exact<{ + name: Types.Scalars['String']['input']; + contactEmail: Types.Scalars['String']['input']; + sponsorshipInterest: Types.Scalars['String']['input']; + website?: Types.InputMaybe; +}>; + + +export type CreateSponsorApplicationMutation = { createSponsorApplication: { __typename: 'CreateSponsorApplicationResult', ok: boolean, code: string | null, message: string | null, sponsor: { __typename: 'SponsorNode', id: string, description: string, name: string, sponsorType: string, status: string, url: string } | null } }; + + +export const CreateSponsorApplicationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateSponsorApplication"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"name"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"contactEmail"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"sponsorshipInterest"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"website"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createSponsorApplication"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"name"},"value":{"kind":"Variable","name":{"kind":"Name","value":"name"}}},{"kind":"Argument","name":{"kind":"Name","value":"contactEmail"},"value":{"kind":"Variable","name":{"kind":"Name","value":"contactEmail"}}},{"kind":"Argument","name":{"kind":"Name","value":"sponsorshipInterest"},"value":{"kind":"Variable","name":{"kind":"Name","value":"sponsorshipInterest"}}},{"kind":"Argument","name":{"kind":"Name","value":"website"},"value":{"kind":"Variable","name":{"kind":"Name","value":"website"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"ok"}},{"kind":"Field","name":{"kind":"Name","value":"code"}},{"kind":"Field","name":{"kind":"Name","value":"message"}},{"kind":"Field","name":{"kind":"Name","value":"sponsor"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sponsorType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/types/__generated__/sponsorQueries.generated.ts b/frontend/src/types/__generated__/sponsorQueries.generated.ts new file mode 100644 index 0000000000..64b964a219 --- /dev/null +++ b/frontend/src/types/__generated__/sponsorQueries.generated.ts @@ -0,0 +1,10 @@ +import * as Types from './graphql'; + +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type GetSponsorsQueryVariables = Types.Exact<{ [key: string]: never; }>; + + +export type GetSponsorsQuery = { sponsors: Array<{ __typename: 'SponsorNode', id: string, description: string, imageUrl: string, name: string, sponsorType: string, status: string, url: string }> }; + + +export const GetSponsorsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSponsors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"sponsors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"sponsorType"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"url"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/frontend/src/types/card.ts b/frontend/src/types/card.ts index 2c21e4c877..aa09b61d89 100644 --- a/frontend/src/types/card.ts +++ b/frontend/src/types/card.ts @@ -16,6 +16,7 @@ import type { PullRequest } from 'types/pullRequest' import type { Release } from 'types/release' import type { ContributionStats } from 'utils/contributionDataUtils' import type { CardType } from 'components/CardDetailsPage' +import type { SponsorInfo } from 'components/SponsorBadge' export type CardProps = { button: Button @@ -84,6 +85,7 @@ export interface DetailsCardProps { topContributors?: Contributor[] topics?: string[] tags?: string[] + sponsors?: SponsorInfo[] type: CardType userSummary?: JSX.Element } diff --git a/frontend/src/types/home.ts b/frontend/src/types/home.ts index 5a3e0a0724..0511bcaf82 100644 --- a/frontend/src/types/home.ts +++ b/frontend/src/types/home.ts @@ -35,8 +35,10 @@ export type MainPageData = { export type Sponsor = { id: string + description?: string imageUrl: string name: string sponsorType: string + status?: string url: string } diff --git a/frontend/src/types/sponsor.ts b/frontend/src/types/sponsor.ts new file mode 100644 index 0000000000..ebf7aebaad --- /dev/null +++ b/frontend/src/types/sponsor.ts @@ -0,0 +1,9 @@ +export type Sponsor = { + id: string + description?: string + imageUrl: string + name: string + sponsorType: string + status?: string + url?: string +}