Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
8296aca
feat: added pages and basic layout for faqs section
twkavya004 May 7, 2026
2f8e524
feat: made branch for opportunity section
MaanasNegi May 13, 2026
f0e2d26
fix: added section back to page after rebase with main
twkavya004 May 13, 2026
79b5fb6
feat: added opportunies available number
twkavya004 May 13, 2026
bf08be0
feat: added header for opportunities table
twkavya004 May 13, 2026
103938f
feat: added sorting/date logic
twkavya004 May 13, 2026
5a479b1
fix: edited table to match sorting logic
twkavya004 May 13, 2026
9aff72d
feat: connected existing modal to table with read more button
twkavya004 May 13, 2026
4c2f2ec
feat: added colour for hover on table rows
twkavya004 May 13, 2026
21e51cc
feat: added logic for type and sort by buttons on header
twkavya004 May 13, 2026
5c4b141
feat: edited show button logic and edited showing opportunities to be…
twkavya004 May 13, 2026
de58bbe
chore: extended fake opportunities list for testing
twkavya004 May 13, 2026
7a00928
fix: fixed spacing and padding to match figma
twkavya004 May 13, 2026
a17a7b2
fead: add pagination controls in UI
twkavya004 May 13, 2026
bc91c85
feat: added pagination logic to buttons
twkavya004 May 13, 2026
28688ad
fix: removed arrow appearence on dropdowns to match figma
twkavya004 May 13, 2026
2845319
feat: matched read more/apply buttons to figma design
twkavya004 May 14, 2026
2c18341
feat: updated arrow to svg to better match figma
twkavya004 May 14, 2026
ac9346e
fix: removed unused modal from page and unused readmore url from oppo…
twkavya004 May 14, 2026
b64ad76
chore: added comment about fake data
twkavya004 May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions public/arrow-up-right.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 2 additions & 1 deletion src/app/(frontend)/components/FAQItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const FAQItem = ({ question, answer }: FAQItemProps) => {
<details className="group border-t border-[#EBEBEB] text-black mx-8 md:mx-20 lg:mx-25 xl:mx-30 py-[24px] cursor-pointer">
<summary className="flex items-center justify-between list-none">
<h3 className="font-medium text-[20px] leading-[32px] text-black">{question}</h3>

<span className="transition-transform duration-300 group-open:rotate-180">
<svg
fill="none"
Expand All @@ -23,7 +24,7 @@ const FAQItem = ({ question, answer }: FAQItemProps) => {
</span>
</summary>

<p className="mt-[16px] text-[20px] leading-[32px] text-gray-600 ">{answer}</p>
<p className="mt-[16px] text-[20px] leading-[32px] text-gray-600">{answer}</p>
</details>
)
}
Expand Down
3 changes: 2 additions & 1 deletion src/app/(frontend)/components/FAQSection.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import FAQItem from './FAQItem'

// Sample FAQ data to be replaced with actual FAQs
const faqData = [
{
id: 1,
Expand Down Expand Up @@ -52,11 +51,13 @@ const FAQSection = () => {
<h2 className="font-semibold text-[40px] leading-[56px] text-black mx-8 md:mx-20 lg:mx-25 xl:mx-30 pt-[116px] pb-[34px]">
FAQs
</h2>

<div>
{faqData.map((item) => (
<FAQItem key={item.id} {...item} />
))}
</div>

<div className="h-[64px]" />
</section>
)
Expand Down
244 changes: 244 additions & 0 deletions src/app/(frontend)/components/OpportunitySection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
'use client'

import { useEffect, useMemo, useState } from 'react'
import OpportunityTable from './OpportunityTable'

// TODO: replace with real data from API

const opportunities = [
{
id: 1,
type: 'Scholarship',
title: 'Lodge of the Liberal Arts: Howard Wyatt Memorial Scholarship',
deadlineLabel: '20th of May, 11:59pm NZST',
deadlineDate: '2026-05-20T23:59:00+12:00',
description:
'The Freemasons of Lodge No.500 have established a trust for charitable purposes, to assist young musicians in their education. Scholarships totalling $3,000 are granted each year to members of AYO who have shown outstanding...',
applyUrl: '#',
},
{
id: 2,
type: 'Scholarship',
title: 'Chip and Muriel Stevens Award',
deadlineLabel: '20th of May, 11:59pm NZST',
deadlineDate: '2026-05-20T23:59:00+12:00',
description:
'This $1,500 award is dedicated to the memory of a former Chairman of AYO, N.W. (Chip) Stevens, who spent his lifetime encouraging young people to love music and young musicians to reach their full potential.',
applyUrl: '#',
},
{
id: 3,
type: 'Competition',
title: 'AYO Soloist Competition',
deadlineLabel: '15th of August, 11:59pm NZST',
deadlineDate: '2026-08-15T23:59:00+12:00',
description:
'The AYO Soloist Competition offers existing orchestra members the chance to compete for monetary prizes and a concerto appearance with the orchestra. The orchestra showcases young soloists and composers; it...',
applyUrl: '#',
},
{
id: 4,
type: 'Scholarship',
title: 'AYO International Performance Grant',
deadlineLabel: '1st of June, 11:59pm NZST',
deadlineDate: '2026-06-01T23:59:00+12:00',
description:
'Supports orchestra members travelling internationally for advanced musical training and performance opportunities.',
applyUrl: '#',
},
{
id: 5,
type: 'Competition',
title: 'Emerging Composer Competition',
deadlineLabel: '10th of July, 11:59pm NZST',
deadlineDate: '2026-07-10T23:59:00+12:00',
description:
'Young composers are invited to submit original orchestral works for adjudication and potential live performance.',
applyUrl: '#',
},
{
id: 6,
type: 'Workshop',
title: 'Conducting Masterclass Programme',
deadlineLabel: '5th of June, 11:59pm NZST',
deadlineDate: '2026-06-05T23:59:00+12:00',
description:
'A practical workshop series led by professional conductors focusing on rehearsal technique, score preparation, and ensemble leadership.',
applyUrl: '#',
},
{
id: 7,
type: 'Scholarship',
title: 'Regional Music Development Scholarship',
deadlineLabel: '25th of May, 11:59pm NZST',
deadlineDate: '2026-05-25T23:59:00+12:00',
description:
'Financial assistance for students from regional communities pursuing advanced orchestral studies.',
applyUrl: '#',
},
{
id: 8,
type: 'Competition',
title: 'Chamber Ensemble Showcase',
deadlineLabel: '30th of September, 11:59pm NZST',
deadlineDate: '2026-09-30T23:59:00+12:00',
description:
'Small ensembles compete for performance opportunities during the annual AYO concert season.',
applyUrl: '#',
},
{
id: 9,
type: 'Residency',
title: 'Composer-in-Residence Programme',
deadlineLabel: '18th of August, 11:59pm NZST',
deadlineDate: '2026-08-18T23:59:00+12:00',
description:
'Selected applicants will collaborate directly with the orchestra over a six-month residency period developing new compositions.',
applyUrl: '#',
},
{
id: 10,
type: 'Workshop',
title: 'Advanced Audition Preparation Intensive',
deadlineLabel: '12th of June, 11:59pm NZST',
deadlineDate: '2026-06-12T23:59:00+12:00',
description:
'An intensive coaching programme helping musicians prepare orchestral excerpts, solo repertoire, and audition strategies.',
applyUrl: '#',
},
]

export default function OpportunitySection() {
const [sortOrder, setSortOrder] = useState<'asc' | 'desc'>('asc')
const [selectedType, setSelectedType] = useState('All')
const [showCount, setShowCount] = useState(5)
const [currentPage, setCurrentPage] = useState(1)

// Dynamically generate available types
const opportunityTypes = ['All', ...new Set(opportunities.map((opp) => opp.type))]

// Filter opportunities
const filteredOpportunities =
selectedType === 'All'
? opportunities
: opportunities.filter((opp) => opp.type === selectedType)

// Sort opportunities
const sortedOpportunities = useMemo(() => {
return [...filteredOpportunities].sort((a, b) => {
return sortOrder === 'asc'
? new Date(a.deadlineDate).getTime() - new Date(b.deadlineDate).getTime()
: new Date(b.deadlineDate).getTime() - new Date(a.deadlineDate).getTime()
})
}, [filteredOpportunities, sortOrder])

// Reset page when controls change
useEffect(() => {
setCurrentPage(1)
}, [selectedType, sortOrder, showCount])

// Pagination
const totalPages = Math.ceil(sortedOpportunities.length / showCount)

const paginatedOpportunities = sortedOpportunities.slice(
(currentPage - 1) * showCount,
currentPage * showCount,
)

return (
<section className="bg-white w-full">
<div className="mx-8 md:mx-20 lg:mx-24 xl:mx-32 pt-[116px] pb-[64px]">
<h2 className="font-semibold text-[40px] leading-[48px] text-black">Opportunities</h2>

<p className="mt-4 text-[18px] leading-[22px] text-[#B2B2B2] italic">
There are a range of opportunities we offer, exclusively to AYO players.
</p>

{/* Controls */}
<div className="flex flex-wrap items-center gap-8 mt-8 text-[15px] leading-[18px]">
{/* Type */}
<div className="flex items-center gap-2">
<label className="text-[#B2B2B2]">Type</label>

<select
value={selectedType}
onChange={(e) => setSelectedType(e.target.value)}
className="font-semibold text-black bg-transparent outline-none appearance-none cursor-pointer pr-4"
>
{opportunityTypes.map((type) => (
<option key={type} value={type}>
{type}
</option>
))}
</select>
</div>

{/* Sort */}
<div className="flex items-center gap-2">
<label className="text-[#B2B2B2]">Sort by</label>

<select
value={sortOrder}
onChange={(e) => setSortOrder(e.target.value as 'asc' | 'desc')}
className="font-semibold text-black bg-transparent outline-none appearance-none cursor-pointer pr-4"
>
<option value="asc">Closing Date (Soonest)</option>
<option value="desc">Closing Date (Latest)</option>
</select>
</div>

{/* Show */}
<div className="flex items-center gap-2">
<label className="text-[#B2B2B2]">Show</label>

<select
value={showCount}
onChange={(e) => setShowCount(Number(e.target.value))}
className="font-semibold text-black bg-transparent outline-none appearance-none cursor-pointer pr-4"
>
<option value={3}>3</option>
<option value={5}>5</option>
<option value={10}>10</option>
</select>
</div>

{/* Count */}
<span className="ml-auto italic font-normal text-[#B7B7B7]">
Showing {paginatedOpportunities.length}{' '}
{paginatedOpportunities.length === 1 ? 'opportunity' : 'opportunities'}
</span>
</div>

<hr className="border-[#EBEBEB] mt-6" />

{/* Table */}
<OpportunityTable opportunities={paginatedOpportunities} />

{/* Pagination */}
<div className="flex items-center justify-between mt-6 text-[15px] leading-[18px]">
<div className="flex gap-12">
<button
onClick={() => setCurrentPage((prev) => Math.max(prev - 1, 1))}
disabled={currentPage === 1}
className="underline disabled:no-underline disabled:opacity-40"
>
Previous
</button>

<button
onClick={() => setCurrentPage((prev) => Math.min(prev + 1, totalPages))}
disabled={currentPage === totalPages}
className="underline disabled:no-underline disabled:opacity-40"
>
Next
</button>
</div>

<span className="font-normal">
{currentPage} of {totalPages}
</span>
</div>
</div>
</section>
)
}
85 changes: 85 additions & 0 deletions src/app/(frontend)/components/OpportunityTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
'use client'

import { useState } from 'react'
import OpportunityModal from './OpportunityModal'
import ArrowUpRight from '/arrow-up-right.svg'

type Opportunity = {
id: number
type: string
title: string
deadlineLabel: string
deadlineDate: string
description: string
applyUrl: string
}

type OpportunityTableProps = {
opportunities: Opportunity[]
}

type OpportunityRowProps = Opportunity & {
onReadMore: () => void
}

const OpportunityRow = ({
title,
deadlineLabel,
description,
applyUrl,
onReadMore,
}: OpportunityRowProps) => {
return (
<div className="grid grid-cols-1 md:grid-cols-[2fr_3fr_1fr] gap-6 md:gap-8 py-8 items-start not-italic transition-colors hover:bg-gray-50">
<div>
<h2 className="font-bold text-base">{title}</h2>

<p className="text-sm text-gray-500 mt-1">Apply by {deadlineLabel}</p>
</div>

<p className="text-sm italic">{description}</p>

<div className="flex gap-6 justify-start md:justify-end">
<button onClick={onReadMore} className="text-sm underline font-bold">
Read More
</button>

<a href={applyUrl} className="text-sm flex items-center underline font-bold">
Apply
<img src="/arrow-up-right.svg" alt="" className="w-[15px] h-[15px]" />
</a>
</div>
</div>
)
}

const OpportunityTable = ({ opportunities }: OpportunityTableProps) => {
const [selectedOpp, setSelectedOpp] = useState<Opportunity | null>(null)

return (
<div className="w-full">
{opportunities.map((opp, index) => (
<div key={opp.id}>
{index > 0 && <hr className="border-gray-200" />}

<OpportunityRow {...opp} onReadMore={() => setSelectedOpp(opp)} />
</div>
))}

{/* Modal */}
{selectedOpp && (
<OpportunityModal
title={selectedOpp.title}
awarded={selectedOpp.type}
value="TBC"
description={selectedOpp.description}
closingDate={selectedOpp.deadlineLabel}
applyUrl={selectedOpp.applyUrl}
onClose={() => setSelectedOpp(null)}
/>
)}
</div>
)
}

export default OpportunityTable
4 changes: 3 additions & 1 deletion src/app/(frontend)/join-ayo/page.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import FAQSection from '../components/FAQSection'
import OpportunitySection from '../components/OpportunitySection'
import OpportunityModal from '../components/OpportunityModal'

export default function JoinAyoPage() {
return (
<main>
{/* <p>This is the join AYO page</p> */}
<OpportunitySection />
<FAQSection />
</main>
)
Expand Down
Loading