Skip to content

Commit 103143c

Browse files
committed
Add home achievements and improve events and contact
1 parent 7ecbba6 commit 103143c

9 files changed

Lines changed: 300 additions & 60 deletions

File tree

‎README.md‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -91,9 +91,14 @@ tablet, and two on mobile.
9191

9292
### Contact form
9393

94-
The contact form opens the visitor's email app with a draft addressed to
95-
`aiclubcfi@smail.iitm.ac.in`. Update `site.json > email` and `site.json > contact.endpoint`
96-
together if the club address changes.
94+
The contact form sends submissions to `aiclubcfi@smail.iitm.ac.in` through
95+
[FormSubmit](https://formsubmit.co/), without opening the visitor's email app. The first
96+
submission triggers a verification email to that inbox; a club mail administrator must
97+
follow its confirmation link before FormSubmit delivers messages. Check the spam folder
98+
if it does not arrive. The form shows sending, success, and error messages, and its
99+
ordinary POST action works when JavaScript is unavailable. If the club address changes,
100+
update `site.json > email`, `site.json > contact.endpoint`, and
101+
`site.json > contact.ajaxEndpoint` together, then verify the new address with FormSubmit.
97102

98103
## GitHub Pages deployment
99104

‎src/components/Contact.astro‎

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ const { reveal } = Astro.props;
2222
</a>
2323
</div>
2424
<div class="contact-panel">
25-
<form id="contact-form" action={contact.endpoint} method="get">
25+
<form id="contact-form" action={contact.endpoint} method="post">
26+
<input type="hidden" name="_subject" value={contact.subject} />
27+
<input type="hidden" name="_honey" tabindex="-1" autocomplete="off" />
2628
<label>
2729
{contact.name}
2830
<input type="text" name="name" autocomplete="name" required />
@@ -40,22 +42,55 @@ const { reveal } = Astro.props;
4042
{contact.submit}
4143
<Arrow />
4244
</button>
45+
<p class="contact-status" role="status" aria-live="polite"></p>
4346
</form>
4447
</div>
4548
</section>
4649

47-
<script define:vars={{ recipient: site.email, subject: contact.subject }}>
48-
document
49-
.querySelector("#contact-form")
50-
?.addEventListener("submit", (event) => {
51-
event.preventDefault();
52-
const form = event.currentTarget;
53-
if (!(form instanceof HTMLFormElement) || !form.reportValidity()) return;
54-
const data = new FormData(form);
55-
const name = String(data.get("name") || "");
56-
const sender = String(data.get("email") || "");
57-
const message = String(data.get("message") || "");
58-
const body = `${message}\n\nFrom: ${name}\nReply to: ${sender}`;
59-
window.location.href = `mailto:${recipient}?subject=${encodeURIComponent(subject)}&body=${encodeURIComponent(body)}`;
60-
});
50+
<script define:vars={{ contact }}>
51+
const form = document.querySelector("#contact-form");
52+
form?.addEventListener("submit", async (event) => {
53+
event.preventDefault();
54+
if (!(form instanceof HTMLFormElement) || !form.reportValidity()) return;
55+
56+
const button = form.querySelector('button[type="submit"]');
57+
const status = form.querySelector(".contact-status");
58+
if (
59+
!(button instanceof HTMLButtonElement) ||
60+
!(status instanceof HTMLElement)
61+
)
62+
return;
63+
64+
const fields = Object.fromEntries(new FormData(form));
65+
fields._replyto = fields.email;
66+
button.disabled = true;
67+
form.setAttribute("aria-busy", "true");
68+
status.textContent = contact.sending;
69+
70+
try {
71+
const response = await fetch(contact.ajaxEndpoint, {
72+
method: "POST",
73+
headers: {
74+
"Content-Type": "application/json",
75+
Accept: "application/json",
76+
},
77+
body: JSON.stringify(fields),
78+
});
79+
const result = await response.json();
80+
if (
81+
!response.ok ||
82+
result.success === false ||
83+
result.success === "false"
84+
) {
85+
throw new Error("FormSubmit rejected the submission");
86+
}
87+
form.reset();
88+
status.textContent = contact.sent;
89+
} catch {
90+
status.textContent = contact.sendError;
91+
} finally {
92+
button.disabled = false;
93+
form.removeAttribute("aria-busy");
94+
}
95+
});
6196
</script>

‎src/components/EventsPage.astro‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ const youtube = site.socials.find((social) => social.label === "YouTube");
2929
<YearNav kind="events" current={year} />
3030
</PageHeading>
3131
<div class="shell archive-content">
32+
{year === "26-27" && upcoming.status === "placeholder" && (
33+
<p class="upcoming-status">
34+
<span>{site.labels.upcomingEvents}</span>
35+
<strong>{site.labels.noUpcomingEvents}</strong>
36+
</p>
37+
)}
3238
{year === "26-27" &&
3339
!records.length &&
3440
upcoming.status !== "placeholder" && (

‎src/components/HackathonCard.astro‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
---
2+
import type achievements from "../data/achievements.json";
3+
import { localUrl } from "../lib/urls";
4+
5+
interface Props {
6+
item: (typeof achievements)["hackathons"][number];
7+
reveal?: boolean;
8+
}
9+
10+
const { item, reveal = false } = Astro.props;
11+
---
12+
13+
<article class="hackathon-card" data-reveal={reveal ? "row" : undefined}>
14+
<div class="hackathon-image">
15+
<img
16+
src={localUrl(item.image)}
17+
alt=""
18+
loading="lazy"
19+
decoding="async"
20+
width="600"
21+
height="300"
22+
/>
23+
</div>
24+
<div class="hackathon-card-body">
25+
<h3>{item.title}</h3>
26+
<p class:list={["hackathon-result", item.result.toLowerCase()]}>
27+
{item.result}
28+
</p>
29+
</div>
30+
</article>

‎src/components/ResearchCard.astro‎

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
---
2+
import type achievements from "../data/achievements.json";
3+
import site from "../data/site.json";
4+
import Arrow from "./Arrow.astro";
5+
6+
interface Props {
7+
item: (typeof achievements)["publications"][number];
8+
reveal?: boolean;
9+
}
10+
11+
const { item, reveal = false } = Astro.props;
12+
---
13+
14+
<article class="research-card card" data-reveal={reveal ? "row" : undefined}>
15+
<h3>
16+
<a href={item.url} target="_blank" rel="noopener noreferrer">
17+
{item.title}
18+
</a>
19+
</h3>
20+
<p>{item.description}</p>
21+
<a
22+
class="text-link"
23+
href={item.url}
24+
target="_blank"
25+
rel="noopener noreferrer"
26+
aria-label={`${site.labels.readArticle}: ${item.title}`}
27+
>
28+
{site.labels.readArticle}
29+
<Arrow diagonal />
30+
</a>
31+
</article>

‎src/data/site.json‎

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"allEvents": "See all events",
9595
"scrollToEvents": "Scroll to Events",
9696
"allBlogs": "See all blogs",
97+
"allAchievements": "See all achievements",
9798
"allProjects": "Explore projects",
9899
"readArticle": "Read article",
99100
"resources": "Resources",
@@ -113,6 +114,8 @@
113114
"pauseVideo": "Pause background animation",
114115
"playVideo": "Play background animation",
115116
"upcoming": "Upcoming event",
117+
"upcomingEvents": "Upcoming events",
118+
"noUpcomingEvents": "No upcoming events",
116119
"announcementPlaceholder": "To be announced",
117120
"upcomingNote": "TODO: Add the next event’s details when confirmed.",
118121
"emptyEvents": "TODO: Add confirmed events for this academic year.",
@@ -139,13 +142,17 @@
139142
"notFoundDescription": "This page may have moved. Explore the site using the navigation above."
140143
},
141144
"contact": {
142-
"endpoint": "mailto:aiclubcfi@smail.iitm.ac.in",
145+
"endpoint": "https://formsubmit.co/aiclubcfi@smail.iitm.ac.in",
146+
"ajaxEndpoint": "https://formsubmit.co/ajax/aiclubcfi@smail.iitm.ac.in",
143147
"name": "Name",
144148
"email": "Email",
145149
"message": "Message",
146150
"submit": "Send message",
147151
"emailAction": "Email the club",
148152
"subject": "AI Club website enquiry",
149-
"privacy": "Submitting opens your email app with the message ready to send."
153+
"privacy": "Your message is sent to the AI Club inbox through FormSubmit.",
154+
"sending": "Sending your message…",
155+
"sent": "Message submitted. Thank you for reaching out!",
156+
"sendError": "The message could not be sent. Please email us using the address on this page."
150157
}
151158
}

‎src/pages/achievements.astro‎

Lines changed: 4 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
---
22
import Layout from "../layouts/Layout.astro";
33
import PageHeading from "../components/PageHeading.astro";
4-
import Arrow from "../components/Arrow.astro";
4+
import ResearchCard from "../components/ResearchCard.astro";
5+
import HackathonCard from "../components/HackathonCard.astro";
56
import achievements from "../data/achievements.json";
67
import site from "../data/site.json";
7-
import { localUrl } from "../lib/urls";
88
---
99

1010
<Layout title={site.labels.achievements}>
@@ -18,24 +18,7 @@ import { localUrl } from "../lib/urls";
1818
{achievements.publications
1919
.filter((item) => item.category === category)
2020
.map((item) => (
21-
<article class="research-card card" data-reveal="row">
22-
<h3>
23-
<a href={item.url} target="_blank" rel="noopener noreferrer">
24-
{item.title}
25-
</a>
26-
</h3>
27-
<p>{item.description}</p>
28-
<a
29-
class="text-link"
30-
href={item.url}
31-
target="_blank"
32-
rel="noopener noreferrer"
33-
aria-label={`${site.labels.readArticle}: ${item.title}`}
34-
>
35-
{site.labels.readArticle}
36-
<Arrow diagonal />
37-
</a>
38-
</article>
21+
<ResearchCard item={item} reveal />
3922
))}
4023
</div>
4124
</section>
@@ -46,23 +29,7 @@ import { localUrl } from "../lib/urls";
4629
</h2>
4730
<div class="hackathon-grid">
4831
{achievements.hackathons.map((item) => (
49-
<article class="hackathon-card" data-reveal="row">
50-
<div class="hackathon-image">
51-
<img
52-
src={localUrl(item.image)}
53-
alt=""
54-
loading="lazy"
55-
width="600"
56-
height="300"
57-
/>
58-
</div>
59-
<div class="hackathon-card-body">
60-
<h3>{item.title}</h3>
61-
<p class:list={["hackathon-result", item.result.toLowerCase()]}>
62-
{item.result}
63-
</p>
64-
</div>
65-
</article>
32+
<HackathonCard item={item} reveal />
6633
))}
6734
</div>
6835
</section>

‎src/pages/index.astro‎

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,16 @@ import Arrow from "../components/Arrow.astro";
88
import site from "../data/site.json";
99
import events from "../data/events.json";
1010
import blogs from "../data/blogs.json";
11+
import achievements from "../data/achievements.json";
12+
import announcements from "../data/announcements.json";
1113
import { newestPublished } from "../lib/content.mjs";
1214
import { localUrl } from "../lib/urls";
1315
const recentEvents = newestPublished(events).slice(0, 3);
1416
const recentBlogs = newestPublished(blogs).slice(0, 3);
17+
const featuredResearch = achievements.publications
18+
.filter((item) => item.category === "Research")
19+
.slice(0, 3);
20+
const featuredHackathons = achievements.hackathons.slice(0, 3);
1521
---
1622

1723
<Layout>
@@ -41,6 +47,12 @@ const recentBlogs = newestPublished(blogs).slice(0, 3);
4147
<Arrow />
4248
</a>
4349
</div>
50+
{announcements.upcomingEvent.status === "placeholder" && (
51+
<p class="upcoming-status">
52+
<span>{site.labels.upcomingEvents}</span>
53+
<strong>{site.labels.noUpcomingEvents}</strong>
54+
</p>
55+
)}
4456
<div class="event-grid">
4557
{recentEvents.map((event) => (
4658
<EventCard event={event} compact />
@@ -61,6 +73,54 @@ const recentBlogs = newestPublished(blogs).slice(0, 3);
6173
))}
6274
</div>
6375
</section>
64-
<Partners reveal="right" />
65-
<Contact reveal="left" />
76+
<section class="section shell home-achievements" id="achievements">
77+
<div class="section-head" data-reveal="right">
78+
<h2 class="section-title">{site.labels.achievements}</h2>
79+
<a class="text-link" href={localUrl("/achievements/")}>
80+
{site.labels.allAchievements}
81+
<Arrow />
82+
</a>
83+
</div>
84+
<div class="home-achievement-layout">
85+
<section class="home-achievement-panel card" data-reveal="left">
86+
<h3>{site.labels.research}</h3>
87+
<ul class="home-research-list">
88+
{featuredResearch.map((item) => (
89+
<li>
90+
<a href={item.url} target="_blank" rel="noopener noreferrer">
91+
{item.title}
92+
<Arrow diagonal />
93+
</a>
94+
<p>{item.description}</p>
95+
</li>
96+
))}
97+
</ul>
98+
</section>
99+
<section class="home-achievement-panel card" data-reveal="right">
100+
<h3>{site.labels.hackathons}</h3>
101+
<ul class="home-award-list">
102+
{featuredHackathons.map((item) => (
103+
<li>
104+
<img
105+
src={localUrl(item.image)}
106+
alt=""
107+
width="112"
108+
height="84"
109+
loading="lazy"
110+
decoding="async"
111+
/>
112+
<div>
113+
<h4>{item.title}</h4>
114+
<p class:list={["hackathon-result", item.result.toLowerCase()]}>
115+
{item.result}
116+
</p>
117+
</div>
118+
</li>
119+
))}
120+
</ul>
121+
</section>
122+
</div>
123+
</section>
124+
<Partners reveal="left" />
125+
<Contact reveal="right" />
66126
</Layout>

0 commit comments

Comments
 (0)