Skip to content

Commit 149cf64

Browse files
committed
🔀 merge branch 'master' into jcailly/fix_aria_required_children
2 parents ab376c0 + 5c65c91 commit 149cf64

File tree

30 files changed

+1404
-67
lines changed

30 files changed

+1404
-67
lines changed

env.mjs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ export const env = createEnv({
55
server: {
66
NODE_ENV: z.enum(["development", "production", "test"]),
77
PORT: z.string().optional(),
8+
JOIN_API_KEY: z.string().optional(),
9+
RESEND_API_KEY: z.string().optional(),
810
},
911
client: {
1012
NEXT_PUBLIC_APP_URL: z.string().optional(),
@@ -14,6 +16,8 @@ export const env = createEnv({
1416
process.env.NEXT_PUBLIC_APP_URL || "https://onruntime.com",
1517
NODE_ENV: process.env.NODE_ENV,
1618
PORT: process.env.PORT,
19+
JOIN_API_KEY: process.env.JOIN_API_KEY,
20+
RESEND_API_KEY: process.env.RESEND_API_KEY,
1721
},
1822
skipValidation: !!process.env.CI,
1923
emptyStringAsUndefined: true,

next-sitemap.config.js

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,42 @@
1-
/* eslint-disable @typescript-eslint/no-require-imports */
2-
const { env } = require("process");
3-
4-
const nextConfig = require("./next.config.mjs").default;
5-
61
/** @type {import('next-sitemap').IConfig} */
7-
module.exports = {
8-
siteUrl: env.NEXT_PUBLIC_APP_URL,
2+
const config = {
3+
siteUrl: process.env.NEXT_PUBLIC_APP_URL || "https://onruntime.com",
94
generateRobotsTxt: true,
105
robotsTxtOptions: {
116
policies:
12-
env.VERCEL_ENV === "production"
7+
process.env.VERCEL_ENV === "production"
138
? [{ userAgent: "*", allow: "/" }]
149
: [{ userAgent: "*", disallow: "/" }],
1510
},
1611
transform: async (config, path) => {
12+
const locales = ["fr"];
13+
const defaultLocale = "fr";
14+
1715
const pathParts = path.split("/").filter(Boolean);
18-
const hasLocale = nextConfig.i18n.locales.includes(pathParts[0]);
16+
const hasLocale = locales.includes(pathParts[0]);
1917
const pathDepth = hasLocale ? pathParts.length - 1 : pathParts.length;
2018

2119
const calculatedPriority = Number(
2220
Math.max(0.1, Math.min(1.0, 1.0 - pathDepth * 0.2)).toFixed(1),
2321
);
2422

25-
const alternateRefs = nextConfig.i18n.locales.map((locale) => {
23+
const alternateRefs = locales.map((locale) => {
2624
const pathParts = path.split("/");
27-
const hasLocale = nextConfig.i18n.locales.includes(pathParts[1]);
25+
const hasLocale = locales.includes(pathParts[1]);
2826
const pathWithoutLocale = hasLocale
2927
? "/" + pathParts.slice(2).join("/")
3028
: path;
3129

32-
if (locale === nextConfig.i18n.defaultLocale) {
30+
if (locale === defaultLocale) {
3331
return {
3432
hreflang: locale,
35-
href: `${env.NEXT_PUBLIC_APP_URL}${pathWithoutLocale}`,
33+
href: `${process.env.NEXT_PUBLIC_APP_URL}${pathWithoutLocale}`,
3634
hrefIsAbsolute: true,
3735
};
3836
} else {
3937
return {
4038
hreflang: locale,
41-
href: `${env.NEXT_PUBLIC_APP_URL}/${locale}${pathWithoutLocale}`,
39+
href: `${process.env.NEXT_PUBLIC_APP_URL}/${locale}${pathWithoutLocale}`,
4240
hrefIsAbsolute: true,
4341
};
4442
}
@@ -52,3 +50,5 @@ module.exports = {
5250
};
5351
},
5452
};
53+
54+
module.exports = config;

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"lint:fix": "next lint --fix"
1212
},
1313
"dependencies": {
14-
"@hookform/resolvers": "^4.0.0",
14+
"@hookform/resolvers": "^5.0.0",
1515
"@mdx-js/loader": "^3.1.0",
1616
"@mdx-js/react": "^3.1.0",
1717
"@next/mdx": "^15.1.7",
@@ -29,6 +29,7 @@
2929
"class-variance-authority": "^0.7.1",
3030
"clsx": "^2.1.1",
3131
"gray-matter": "^4.0.3",
32+
"next-mdx-remote": "^5.0.0",
3233
"lucide-react": "^0.515.0",
3334
"motion": "^12.0.0",
3435
"next": "15.3.3",

src/app/agency/[city]/page.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ export default async function CityPage({ params }: AgencyPageProps) {
7272
];
7373

7474
return (
75-
<main className="min-h-screen pt-32 pb-16">
75+
<main className="min-h-screen pt-32 pb-16 w-full">
7676

7777
<LocalBusinessSchema
7878
type="ProfessionalService"

src/app/agency/page.tsx

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export default function AgencyLandingPage() {
2020
const majorAgencies = getMajorAgencies(10);
2121

2222
return (
23-
<main className="min-h-screen pt-32 pb-16">
23+
<main className="min-h-screen pt-32 pb-16 sm:w-auto w-full">
2424
<OrganizationSchema
2525
type="DigitalAgency"
2626
id="https://onruntime.com/agency#organization"
@@ -39,12 +39,12 @@ export default function AgencyLandingPage() {
3939
</p>
4040

4141
<div className="flex flex-wrap gap-3">
42-
<Link href={Routes.contact}>
43-
<Button size="lg">
42+
<Button asChild size="lg">
43+
<Link href={Routes.contact}>
4444
Discuter de votre projet
4545
<ArrowRight className="ml-2 w-5 h-5" />
46-
</Button>
47-
</Link>
46+
</Link>
47+
</Button>
4848

4949
<Link href="#expertise-locale">
5050
<Button variant="outline" size="lg">
@@ -264,17 +264,17 @@ export default function AgencyLandingPage() {
264264
Contactez-nous pour discuter de votre projet. Notre expertise des marchés locaux nous permet de vous accompagner efficacement, où que vous soyez en France.
265265
</p>
266266
<div className="flex flex-wrap gap-4">
267-
<Link href={Routes.contact}>
268-
<Button size="lg">
267+
<Button asChild size="lg">
268+
<Link href={Routes.contact}>
269269
Nous contacter
270270
<ArrowRight className="ml-2 w-5 h-5" />
271-
</Button>
272-
</Link>
273-
<Link href="#expertise-locale">
274-
<Button variant="outline" size="lg">
275-
En savoir plus sur notre approche
276-
</Button>
277-
</Link>
271+
</Link>
272+
</Button>
273+
<Button asChild variant="outline" size="lg" className="min-w-[223px] text-md xs:text-xl whitespace-normal xs:whitespace-nowrap ">
274+
<Link href="#expertise-locale">
275+
En savoir plus sur notre approche
276+
</Link>
277+
</Button>
278278
</div>
279279
</div>
280280
</div>

src/app/api/careers/[id]/route.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { NextRequest, NextResponse } from "next/server";
2+
import { z } from "zod";
3+
import { unstable_cache } from "next/cache";
4+
import { formatEmploymentType, formatSalary, extractTags } from "@/lib/utils/careers";
5+
6+
// Validation schema for individual job response
7+
const joinJobSchema = z.object({
8+
id: z.union([z.string(), z.number()]).transform(val => String(val)),
9+
title: z.string(),
10+
department: z.object({
11+
name: z.string(),
12+
}).optional(),
13+
location: z.object({
14+
name: z.string(),
15+
}).optional(),
16+
employmentType: z.string().optional(),
17+
publishedAt: z.string().optional(),
18+
applyUrl: z.string().optional(),
19+
seniority: z.object({
20+
name: z.string(),
21+
}).optional(),
22+
remote: z.boolean().optional(),
23+
shortDescription: z.string().optional(),
24+
description: z.string().optional(),
25+
requirements: z.string().optional(),
26+
benefits: z.string().optional(),
27+
salaryMin: z.number().optional(),
28+
salaryMax: z.number().optional(),
29+
salaryCurrency: z.string().optional(),
30+
validThrough: z.string().optional(),
31+
skills: z.array(z.object({
32+
name: z.string(),
33+
})).optional(),
34+
});
35+
36+
// Cached function to fetch individual job from join.com API
37+
const getCachedJob = unstable_cache(
38+
async (jobId: string) => {
39+
const apiKey = process.env.JOIN_API_KEY;
40+
if (!apiKey) {
41+
throw new Error("JOIN_API_KEY is not configured");
42+
}
43+
44+
const response = await fetch(`https://api.join.com/v2/jobs/${jobId}`, {
45+
headers: {
46+
Authorization: apiKey,
47+
Accept: "application/json",
48+
},
49+
});
50+
51+
if (response.status === 404) {
52+
return null; // Job not found
53+
}
54+
55+
if (!response.ok) {
56+
const errorText = await response.text();
57+
console.error(`Join API error - Status: ${response.status}, Response: ${errorText}`);
58+
throw new Error(`Join API responded with status: ${response.status}`);
59+
}
60+
61+
const jobData = await response.json();
62+
63+
// Validate the response data
64+
const validationResult = joinJobSchema.safeParse(jobData);
65+
if (!validationResult.success) {
66+
console.error("Invalid job response from join.com API:", validationResult.error);
67+
throw new Error("Invalid job data format from join.com API");
68+
}
69+
70+
const validatedJob = validationResult.data;
71+
return {
72+
id: validatedJob.id,
73+
title: validatedJob.title,
74+
department: validatedJob.department?.name || "Non spécifié",
75+
location: validatedJob.location?.name || "Remote",
76+
employmentType:
77+
formatEmploymentType(validatedJob.employmentType) || "Temps plein",
78+
datePosted: validatedJob.publishedAt || new Date().toISOString(),
79+
applyUrl:
80+
validatedJob.applyUrl ||
81+
`https://join.com/companies/onruntime/jobs/${validatedJob.id}`,
82+
seniority: validatedJob.seniority?.name || null,
83+
remote: validatedJob.remote || false,
84+
shortDescription: validatedJob.shortDescription || "",
85+
description: validatedJob.description || "",
86+
requirements: validatedJob.requirements || "",
87+
benefits: validatedJob.benefits || "",
88+
salary:
89+
formatSalary(
90+
validatedJob.salaryMin,
91+
validatedJob.salaryMax,
92+
validatedJob.salaryCurrency,
93+
) || null,
94+
validThrough: validatedJob.validThrough || null,
95+
tags: extractTags(validatedJob),
96+
};
97+
},
98+
['join-job'],
99+
{
100+
revalidate: 300, // 5 minutes cache
101+
tags: ['careers'],
102+
}
103+
);
104+
105+
export async function GET(request: NextRequest, { params }: { params: Promise<{ id: string }> }) {
106+
try {
107+
const { id } = await params;
108+
109+
if (!id) {
110+
return NextResponse.json(
111+
{ error: "Job ID is required" },
112+
{ status: 400 },
113+
);
114+
}
115+
116+
const job = await getCachedJob(id);
117+
118+
if (!job) {
119+
return NextResponse.json({ error: "Job not found" }, { status: 404 });
120+
}
121+
122+
return NextResponse.json({ job });
123+
} catch (error) {
124+
console.error("Error fetching job from Join API:", error);
125+
126+
// Check if this is a network error or API error
127+
if (error instanceof Error) {
128+
console.error("Error details:", error.message);
129+
}
130+
131+
return NextResponse.json(
132+
{ error: "Failed to fetch job details" },
133+
{ status: 500 },
134+
);
135+
}
136+
}
137+
138+

0 commit comments

Comments
 (0)