diff --git a/backend/apps/api/rest/v0/project.py b/backend/apps/api/rest/v0/project.py index 7b26c3315a..e89bb19506 100644 --- a/backend/apps/api/rest/v0/project.py +++ b/backend/apps/api/rest/v0/project.py @@ -12,9 +12,11 @@ from apps.api.decorators.cache import cache_response from apps.api.rest.v0.common import Leader, ValidationErrorSchema +from apps.api.rest.v0.sponsor import Sponsor 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": { @@ -145,3 +147,18 @@ def get_project( return project return Response({"message": "Project not found"}, status=HTTPStatus.NOT_FOUND) + + +@router.get( + "/nest/sponsors", + description="Retrieve sponsors associated with project nest.", + operation_id="list_nest_sponsors", + response=list[Sponsor], + summary="List nest sponsors", +) +@decorate_view(cache_response()) +def list_nest_sponsors( + request: HttpRequest, +) -> list[Sponsor]: + """Get sponsors for a project.""" + return SponsorModel.objects.filter(status=SponsorModel.StatusType.ACTIVE) diff --git a/backend/apps/owasp/admin/sponsor.py b/backend/apps/owasp/admin/sponsor.py index c124b7cd67..f7e5fe8b60 100644 --- a/backend/apps/owasp/admin/sponsor.py +++ b/backend/apps/owasp/admin/sponsor.py @@ -50,13 +50,7 @@ class SponsorAdmin(admin.ModelAdmin, StandardOwaspAdminMixin): ), ( "Status", - { - "fields": ( - "is_member", - "member_type", - "sponsor_type", - ) - }, + {"fields": ("is_member", "member_type", "sponsor_type", "status")}, ), ) 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..8d8d00c7df --- /dev/null +++ b/backend/apps/owasp/api/internal/mutations/__init__.py @@ -0,0 +1,3 @@ +"""Sponsor Mutations.""" + +from .sponsor import SponsorMutation 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..ea49a86bad --- /dev/null +++ b/backend/apps/owasp/api/internal/mutations/sponsor.py @@ -0,0 +1,40 @@ +"""OWASP sponsors GraphQL mutations.""" + +import logging + +import strawberry +from django.db import IntegrityError +from django.utils import timezone + +from apps.common.utils import slugify +from apps.owasp.api.internal.nodes.sponsor import CreateSponsorInput, SponsorNode +from apps.owasp.models import Sponsor + +logger = logging.getLogger(__name__) + +SPONSOR_EXISTS_ERROR = "A sponsor with this name already exists." + + +@strawberry.type +class SponsorMutation: + """GraphQL mutations related to sponsor.""" + + @strawberry.mutation + def create_sponsor(self, input_data: CreateSponsorInput) -> SponsorNode: + """Create a new sponsor.""" + try: + sponsor = Sponsor.objects.create( + key=slugify(input_data.name), + sort_name=input_data.name, + name=input_data.name, + contact_email=input_data.contact_email, + message=input_data.message, + url=input_data.url, + created_at=timezone.now(), + ) + except IntegrityError as err: + raise ValueError(SPONSOR_EXISTS_ERROR) from err + + logger.info("Sponsor created successfully") + + return sponsor diff --git a/backend/apps/owasp/api/internal/nodes/sponsor.py b/backend/apps/owasp/api/internal/nodes/sponsor.py index c66cb269b3..5c9bfc824a 100644 --- a/backend/apps/owasp/api/internal/nodes/sponsor.py +++ b/backend/apps/owasp/api/internal/nodes/sponsor.py @@ -8,12 +8,17 @@ @strawberry_django.type( Sponsor, - fields=[ - "image_url", - "name", - "sponsor_type", - "url", - ], + fields=["image_url", "name", "sponsor_type", "url", "description"], ) class SponsorNode(strawberry.relay.Node): """Sponsor node.""" + + +@strawberry.input +class CreateSponsorInput: + """Input Node for creating a sponsor.""" + + message: str + name: str + url: str + contact_email: str diff --git a/backend/apps/owasp/api/internal/queries/sponsor.py b/backend/apps/owasp/api/internal/queries/sponsor.py index 80014e529f..2174fdcacb 100644 --- a/backend/apps/owasp/api/internal/queries/sponsor.py +++ b/backend/apps/owasp/api/internal/queries/sponsor.py @@ -25,3 +25,18 @@ def sponsors(self) -> list[SponsorNode]: Sponsor.SponsorType.NOT_SPONSOR: 6, }[x.sponsor_type], ) + + @strawberry_django.field + def active_sponsors(self) -> list[SponsorNode]: + """Resolve active sponsors ordered by Sponsor level.""" + return sorted( + Sponsor.objects.filter(status=Sponsor.StatusType.ACTIVE), + key=lambda x: { + Sponsor.SponsorType.DIAMOND: 1, + Sponsor.SponsorType.PLATINUM: 2, + Sponsor.SponsorType.GOLD: 3, + Sponsor.SponsorType.SILVER: 4, + Sponsor.SponsorType.SUPPORTER: 5, + Sponsor.SponsorType.NOT_SPONSOR: 6, + }[x.sponsor_type], + ) diff --git a/backend/apps/owasp/migrations/0073_sponsor_contact_email_sponsor_created_at.py b/backend/apps/owasp/migrations/0073_sponsor_contact_email_sponsor_created_at.py new file mode 100644 index 0000000000..01a0b8bb09 --- /dev/null +++ b/backend/apps/owasp/migrations/0073_sponsor_contact_email_sponsor_created_at.py @@ -0,0 +1,22 @@ +# Generated by Django 6.0.3 on 2026-03-13 11:38 + +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="contact_email", + field=models.EmailField(blank=True, max_length=254), + ), + migrations.AddField( + model_name="sponsor", + name="created_at", + field=models.DateTimeField(blank=True, null=True, verbose_name="Created At"), + ), + ] diff --git a/backend/apps/owasp/migrations/0074_sponsor_chapter_sponsor_project_sponsor_status.py b/backend/apps/owasp/migrations/0074_sponsor_chapter_sponsor_project_sponsor_status.py new file mode 100644 index 0000000000..d25891b512 --- /dev/null +++ b/backend/apps/owasp/migrations/0074_sponsor_chapter_sponsor_project_sponsor_status.py @@ -0,0 +1,36 @@ +# Generated by Django 6.0.3 on 2026-03-13 11:58 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0073_sponsor_contact_email_sponsor_created_at"), + ] + + operations = [ + migrations.AddField( + model_name="sponsor", + name="chapter", + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.SET_NULL, to="owasp.chapter" + ), + ), + migrations.AddField( + model_name="sponsor", + name="project", + field=models.ForeignKey( + null=True, on_delete=django.db.models.deletion.SET_NULL, to="owasp.project" + ), + ), + migrations.AddField( + model_name="sponsor", + name="status", + field=models.CharField( + choices=[("Draft", "Draft"), ("Active", "Active"), ("Archived", "Archived")], + default="Draft", + max_length=20, + ), + ), + ] diff --git a/backend/apps/owasp/migrations/0075_sponsor_message.py b/backend/apps/owasp/migrations/0075_sponsor_message.py new file mode 100644 index 0000000000..2a6209a4a9 --- /dev/null +++ b/backend/apps/owasp/migrations/0075_sponsor_message.py @@ -0,0 +1,17 @@ +# Generated by Django 6.0.3 on 2026-03-21 14:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("owasp", "0074_sponsor_chapter_sponsor_project_sponsor_status"), + ] + + operations = [ + migrations.AddField( + model_name="sponsor", + name="message", + field=models.TextField(blank=True, verbose_name="Message"), + ), + ] diff --git a/backend/apps/owasp/models/sponsor.py b/backend/apps/owasp/models/sponsor.py index d7d0e38158..3359499cdd 100644 --- a/backend/apps/owasp/models/sponsor.py +++ b/backend/apps/owasp/models/sponsor.py @@ -37,11 +37,21 @@ class MemberType(models.TextChoices): GOLD = "Gold" SILVER = "Silver" + class StatusType(models.TextChoices): + """Status type 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) name = models.CharField(verbose_name="Name", max_length=255) sort_name = models.CharField(verbose_name="Sort Name", max_length=255) + created_at = models.DateTimeField(verbose_name="Created At", null=True, blank=True) + contact_email = models.EmailField(blank=True) + message = models.TextField(verbose_name="Message", blank=True) # URLs and images url = models.URLField(verbose_name="Website URL", blank=True) @@ -63,6 +73,12 @@ class MemberType(models.TextChoices): choices=SponsorType.choices, default=SponsorType.NOT_SPONSOR, ) + status = models.CharField(max_length=20, choices=StatusType.choices, default=StatusType.DRAFT) + + # FKs + chapter = models.ForeignKey("owasp.Chapter", on_delete=models.SET_NULL, null=True, blank=True) + + project = models.ForeignKey("owasp.Project", on_delete=models.SET_NULL, null=True, blank=True) def __str__(self) -> str: """Sponsor human readable representation.""" diff --git a/backend/settings/graphql.py b/backend/settings/graphql.py index 4f7fd8691a..f0ddceba96 100644 --- a/backend/settings/graphql.py +++ b/backend/settings/graphql.py @@ -18,16 +18,12 @@ ProgramQuery, ) from apps.nest.api.internal.mutations import NestMutations +from apps.owasp.api.internal.mutations import SponsorMutation from apps.owasp.api.internal.queries import OwaspQuery @strawberry.type -class Mutation( - ApiMutations, - ModuleMutation, - NestMutations, - ProgramMutation, -): +class Mutation(ApiMutations, ModuleMutation, NestMutations, ProgramMutation, SponsorMutation): """Schema mutations.""" diff --git a/docker-compose/local/compose.yaml b/docker-compose/local/compose.yaml index e72fc74855..3aa1368566 100644 --- a/docker-compose/local/compose.yaml +++ b/docker-compose/local/compose.yaml @@ -31,7 +31,7 @@ services: - 8000:8000 volumes: - ../../backend:/home/owasp - - backend-venv:/home/owasp/.venv + - backend-venv_4259:/home/owasp/.venv cache: command: > @@ -50,7 +50,7 @@ services: networks: - nest-network volumes: - - cache-data:/data + - cache-data_4259:/data db: container_name: nest-db @@ -67,7 +67,7 @@ services: networks: - nest-network volumes: - - db-data:/var/lib/postgresql/data + - db-data_4259:/var/lib/postgresql/data docs: container_name: nest-docs @@ -89,7 +89,7 @@ services: - ../../README.md:/home/owasp/README.md:ro - ../../CODE_OF_CONDUCT.md:/home/owasp/CODE_OF_CONDUCT.md:ro - ../../CONTRIBUTING.md:/home/owasp/CONTRIBUTING.md:ro - - docs-venv:/home/owasp/.venv + - docs-venv_4259:/home/owasp/.venv frontend: container_name: nest-frontend @@ -111,8 +111,8 @@ services: - 3000:3000 volumes: - ../../frontend:/home/owasp - - frontend-next:/home/owasp/.next - - frontend-node-modules:/home/owasp/node_modules + - frontend-next_4259:/home/owasp/.next + - frontend-node-modules_4259:/home/owasp/node_modules worker: container_name: nest-worker @@ -141,15 +141,15 @@ services: - nest-network volumes: - ../../backend:/home/owasp - - backend-venv:/home/owasp/.venv + - backend-venv_4259:/home/owasp/.venv networks: nest-network: volumes: - backend-venv: - cache-data: - db-data: - docs-venv: - frontend-next: - frontend-node-modules: + backend-venv_4259: + cache-data_4259: + db-data_4259: + docs-venv_4259: + frontend-next_4259: + frontend-node-modules_4259: diff --git a/frontend/package.json b/frontend/package.json index e3bbeadbd3..34d693dd29 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -62,7 +62,8 @@ "react-icons": "^5.6.0", "react-leaflet": "^5.0.0", "react-leaflet-cluster": "^4.1.3", - "tailwind-merge": "^3.5.0" + "tailwind-merge": "^3.5.0", + "validator": "^13.15.26" }, "devDependencies": { "@axe-core/react": "^4.11.1", @@ -89,6 +90,7 @@ "@types/node": "^25.5.0", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", + "@types/validator": "^13.15.10", "@typescript-eslint/eslint-plugin": "^8.58.0", "@typescript-eslint/parser": "^8.58.0", "eslint": "^10.0.3", diff --git a/frontend/pnpm-lock.yaml b/frontend/pnpm-lock.yaml index c352494174..d9f9ab9bff 100644 --- a/frontend/pnpm-lock.yaml +++ b/frontend/pnpm-lock.yaml @@ -151,6 +151,9 @@ importers: tailwind-merge: specifier: ^3.5.0 version: 3.5.0 + validator: + specifier: ^13.15.26 + version: 13.15.26 devDependencies: '@axe-core/react': specifier: ^4.11.1 @@ -224,6 +227,9 @@ importers: '@types/react-dom': specifier: ^19.2.3 version: 19.2.3(@types/react@19.2.14) + '@types/validator': + specifier: ^13.15.10 + version: 13.15.10 '@typescript-eslint/eslint-plugin': specifier: ^8.58.0 version: 8.58.0(@typescript-eslint/parser@8.58.0(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3))(eslint@10.0.3(jiti@2.6.1))(typescript@5.9.3) @@ -3528,6 +3534,9 @@ packages: '@types/trusted-types@2.0.7': resolution: {integrity: sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==} + '@types/validator@13.15.10': + resolution: {integrity: sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==} + '@types/ws@8.18.1': resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} @@ -7559,6 +7568,10 @@ packages: resolution: {integrity: sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==} engines: {node: '>=10.12.0'} + validator@13.15.26: + resolution: {integrity: sha512-spH26xU080ydGggxRyR1Yhcbgx+j3y5jbNXk/8L+iRvdIEQ4uTRH2Sgf2dokud6Q4oAtsbNvJ1Ft+9xmm6IZcA==} + engines: {node: '>= 0.10'} + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -12086,6 +12099,8 @@ snapshots: '@types/trusted-types@2.0.7': optional: true + '@types/validator@13.15.10': {} + '@types/ws@8.18.1': dependencies: '@types/node': 25.5.0 @@ -16659,6 +16674,8 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 + validator@13.15.26: {} + vary@1.1.2: {} w3c-xmlserializer@5.0.0: diff --git a/frontend/src/app/sponsors/apply/page.tsx b/frontend/src/app/sponsors/apply/page.tsx new file mode 100644 index 0000000000..f55a31bf54 --- /dev/null +++ b/frontend/src/app/sponsors/apply/page.tsx @@ -0,0 +1,70 @@ +'use client' +import { useMutation } from '@apollo/client/react' +import { addToast } from '@heroui/toast' +import { useRouter } from 'next/navigation' +import React, { useState } from 'react' + +import { CreateSponsorDocument } from 'types/__generated__/sponsorMutations.generated' +import SponsorForm from 'components/SponsorForm' + +const SponsorApplicationPage = () => { + const router = useRouter() + const [createSponsor, { loading }] = useMutation(CreateSponsorDocument) + + const [formData, setFormData] = useState({ + name: '', + website: '', + contactEmail: '', + message: '', + }) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + + try { + const input = { + name: formData.name, + url: formData.website, + contactEmail: formData.contactEmail, + message: formData.message, + } + + await createSponsor({ + variables: { input }, + }) + + addToast({ + description: 'Sponsorship application submitted successfully!', + title: 'Success', + timeout: 3000, + shouldShowTimeoutProgress: true, + color: 'success', + variant: 'solid', + }) + + router.push('/sponsors') + } catch (err) { + addToast({ + description: + err instanceof Error ? err.message : 'Unable to complete the requested operation.', + title: 'GraphQL Request Failed', + timeout: 3000, + shouldShowTimeoutProgress: true, + color: 'danger', + variant: 'solid', + }) + } + } + + return ( + + ) +} + +export default SponsorApplicationPage diff --git a/frontend/src/app/sponsors/page.tsx b/frontend/src/app/sponsors/page.tsx new file mode 100644 index 0000000000..a2af5a4c1d --- /dev/null +++ b/frontend/src/app/sponsors/page.tsx @@ -0,0 +1,119 @@ +'use client' +import { useQuery } from '@apollo/client/react' +import Image from 'next/image' +import Link from 'next/link' +import { useRouter } from 'next/navigation' +import { FaBuilding, FaHandHoldingHeart } from 'react-icons/fa' +import { FaArrowUpRightFromSquare } from 'react-icons/fa6' +import { GetSponsorsDataDocument } from 'types/__generated__/sponsorQueries.generated' +import SecondaryCard from 'components/SecondaryCard' + +type Sponsor = { + id: string + name: string + url: string + description: string + imageUrl: string +} + +const SponsorLogo = ({ sponsor }: { sponsor: Sponsor }) => { + if (sponsor.imageUrl) { + return ( +
+ {`${sponsor.name} +
+ ) + } + + return ( +
+
+ ) +} + +const SponsorEntryCard = ({ sponsor }: { sponsor: Sponsor }) => ( +
+
+
+ +
+
+ {sponsor.name} +
+ {sponsor.description && ( +

+ {sponsor.description} +

+ )} +
+ {sponsor.url && ( + + Visit website +
+) + +export default function SponsorsPage() { + const router = useRouter() + + const { data: sponsorsData, loading, error } = useQuery(GetSponsorsDataDocument) + + if (loading) { + return ( +
+ Loading sponsors... +
+ ) + } + + if (error) { + return ( +
+ Failed to load sponsors. Please try again later. +
+ ) + } + + return ( +
+ {sponsorsData?.activeSponsors && sponsorsData?.activeSponsors.length > 0 ? ( +
+ {sponsorsData.activeSponsors.map((sponsor: Sponsor) => ( + + ))} +
+ ) : ( +

No Sponsors Found

+ )} + +
+ +
+
+
+ ) +} diff --git a/frontend/src/components/SponsorForm.tsx b/frontend/src/components/SponsorForm.tsx new file mode 100644 index 0000000000..5691eccad9 --- /dev/null +++ b/frontend/src/components/SponsorForm.tsx @@ -0,0 +1,168 @@ +'use client' + +import React, { useState } from 'react' +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 { + validateName, + validateWebsite, + validateContactEmail, + validateMessage, +} from 'components/forms/shared/formValidationUtils' +import { useFormValidation } from 'components/forms/shared/useFormValidation' + +interface SponsorFormProps { + formData: { + name: string + website: string + contactEmail: string + message: string + } + setFormData: React.Dispatch> + onSubmit: (e: React.FormEvent) => void + loading: boolean + title: string + submitText?: string +} + +const SponsorForm = ({ + formData, + setFormData, + onSubmit, + loading, + title, + submitText = 'Submit', +}: SponsorFormProps) => { + const [touched, setTouched] = useState>({}) + + const handleInputChange = (name: keyof SponsorFormProps['formData'], value: string) => { + setFormData((prev) => ({ ...prev, [name]: value })) + } + + const errors = useFormValidation( + [ + { + field: 'name', + shouldValidate: touched.name ?? false, + validator: () => validateName(formData.name), + }, + { + field: 'website', + shouldValidate: touched.website ?? false, + validator: () => validateWebsite(formData.website), + }, + { + field: 'contactEmail', + shouldValidate: touched.contactEmail ?? false, + validator: () => validateContactEmail(formData.contactEmail), + }, + { + field: 'message', + shouldValidate: touched.message ?? false, + validator: () => validateMessage(formData.message), + }, + ], + [formData, touched] + ) + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault() + + const nextTouched = { + name: true, + website: true, + contactEmail: true, + message: true, + } + setTouched(nextTouched) + + if ( + validateName(formData.name) || + validateWebsite(formData.website) || + validateContactEmail(formData.contactEmail) || + validateMessage(formData.message) + ) { + return + } + + onSubmit(e) + } + + return ( + +
+
+
+
+ + +
+ ) +} + +export default SponsorForm diff --git a/frontend/src/components/forms/shared/formValidationUtils.ts b/frontend/src/components/forms/shared/formValidationUtils.ts index 48735d35d0..05abd9900b 100644 --- a/frontend/src/components/forms/shared/formValidationUtils.ts +++ b/frontend/src/components/forms/shared/formValidationUtils.ts @@ -1,3 +1,4 @@ +import validator from 'validator' import type { ValidationRule } from 'components/forms/shared/useFormValidation' export const validateRequired = (value: string, fieldName: string): string | undefined => { @@ -32,6 +33,39 @@ export const validateEndDate = (value: string, startDate?: string): string | und return undefined } +export const validateWebsite = (value: string): string | undefined => { + const requiredError = validateRequired(value, 'Website') + if (requiredError) return requiredError + + const trimValue = value.trim() + + const normalizedValue = trimValue.toLowerCase() + if (!normalizedValue.startsWith('http://') && !normalizedValue.startsWith('https://')) { + return 'Website must start with http:// or https://' + } + + try { + new URL(trimValue) + } catch { + return 'Enter a valid website URL' + } + + return undefined +} + +export const validateContactEmail = (value: string): string | undefined => { + const requiredError = validateRequired(value, 'Contact email') + if (requiredError) return requiredError + if (!validator.isEmail(value.trim())) { + return 'Enter a valid email' + } + return undefined +} + +export const validateMessage = (value: string): string | undefined => { + return validateRequired(value, 'Message') +} + type CommonFormData = { name: string description: string diff --git a/frontend/src/server/mutations/sponsorMutations.ts b/frontend/src/server/mutations/sponsorMutations.ts new file mode 100644 index 0000000000..a7ce9b2d77 --- /dev/null +++ b/frontend/src/server/mutations/sponsorMutations.ts @@ -0,0 +1,11 @@ +import { gql } from '@apollo/client' + +export const CREATE_SPONSOR = gql` + mutation CreateSponsor($input: CreateSponsorInput!) { + createSponsor(inputData: $input) { + id + name + url + } + } +` diff --git a/frontend/src/server/queries/sponsorQueries.ts b/frontend/src/server/queries/sponsorQueries.ts new file mode 100644 index 0000000000..42b5474dfb --- /dev/null +++ b/frontend/src/server/queries/sponsorQueries.ts @@ -0,0 +1,13 @@ +import { gql } from '@apollo/client' + +export const GET_SPONSORS = gql` + query GetSponsorsData { + activeSponsors { + id + name + url + description + imageUrl + } + } +` diff --git a/frontend/src/types/__generated__/graphql.ts b/frontend/src/types/__generated__/graphql.ts index 0e0cb4ff2a..c279321442 100644 --- a/frontend/src/types/__generated__/graphql.ts +++ b/frontend/src/types/__generated__/graphql.ts @@ -146,6 +146,13 @@ export type CreateProgramInput = { tags?: Array; }; +export type CreateSponsorInput = { + contactEmail: Scalars['String']['input']; + message: Scalars['String']['input']; + name: Scalars['String']['input']; + url: Scalars['String']['input']; +}; + export type EntityMemberNode = Node & { __typename?: 'EntityMemberNode'; description: Scalars['String']['output']; @@ -387,6 +394,7 @@ export type Mutation = { createApiKey: CreateApiKeyResult; createModule: ModuleNode; createProgram: ProgramNode; + createSponsor: SponsorNode; deleteModule: Scalars['String']['output']; githubAuth: GitHubAuthResult; logoutUser: LogoutResult; @@ -431,6 +439,11 @@ export type MutationCreateProgramArgs = { }; +export type MutationCreateSponsorArgs = { + inputData: CreateSponsorInput; +}; + + export type MutationDeleteModuleArgs = { moduleKey: Scalars['String']['input']; programKey: Scalars['String']['input']; @@ -708,6 +721,7 @@ export type PullRequestNode = Node & { export type Query = { __typename?: 'Query'; activeApiKeyCount: Scalars['Int']['output']; + activeSponsors: Array; apiKeys: Array; boardOfDirectors?: Maybe; boardsOfDirectors: Array; @@ -1054,6 +1068,7 @@ export type SnapshotNode = Node & { export type SponsorNode = Node & { __typename?: 'SponsorNode'; + description: Scalars['String']['output']; /** The Globally Unique ID of this object */ id: Scalars['ID']['output']; imageUrl: Scalars['String']['output']; diff --git a/frontend/src/types/__generated__/sponsorMutations.generated.ts b/frontend/src/types/__generated__/sponsorMutations.generated.ts new file mode 100644 index 0000000000..1730545ec5 --- /dev/null +++ b/frontend/src/types/__generated__/sponsorMutations.generated.ts @@ -0,0 +1,12 @@ +import * as Types from './graphql'; + +import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/core'; +export type CreateSponsorMutationVariables = Types.Exact<{ + input: Types.CreateSponsorInput; +}>; + + +export type CreateSponsorMutation = { createSponsor: { __typename: 'SponsorNode', id: string, name: string, url: string } }; + + +export const CreateSponsorDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateSponsor"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateSponsorInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createSponsor"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"inputData"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"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..739881df01 --- /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 GetSponsorsDataQueryVariables = Types.Exact<{ [key: string]: never; }>; + + +export type GetSponsorsDataQuery = { activeSponsors: Array<{ __typename: 'SponsorNode', id: string, name: string, url: string, description: string, imageUrl: string }> }; + + +export const GetSponsorsDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetSponsorsData"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"activeSponsors"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"url"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"imageUrl"}}]}}]}}]} as unknown as DocumentNode; \ No newline at end of file