Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ jobs:
node-version-file: '.nvmrc'
cache: 'npm'

- name: Install dependencies for web
run: npm install --filter=@tech-companies-portugal/web

- name: Cache Playwright browsers
id: playwright-cache
uses: actions/cache@v3
Expand All @@ -72,9 +75,6 @@ jobs:
if: steps.playwright-cache.outputs.cache-hit != 'true'
run: npx playwright install chromium --with-deps

- name: Install dependencies for web
run: npm install --filter=@tech-companies-portugal/web

- name: Run Playwright tests
run: npm run test:e2e:web

Expand Down
23 changes: 18 additions & 5 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
{
"editor.defaultFormatter": "biomejs.biome",

"[javascript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
},
"editor.codeActionsOnSave": {
"source.organizeImports": "always",
"source.fixAll.biome": "explicit",
}
}
"quickfix.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"editor.formatOnSave": true,
"typescript.tsdk": "node_modules/typescript/lib",
}
2 changes: 1 addition & 1 deletion apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,4 +57,4 @@
"@types/react-dom": "19.0.2",
"react-is": "19.0.0"
}
}
}
16 changes: 10 additions & 6 deletions apps/web/src/app/(companies-list)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import CompaniesHeader from "@/components/CompaniesHeader";
import Footer from "@/components/Footer";
import { Skeleton } from "@/components/ui/skeleton";
import { LayoutProps } from "@/lib/types";
import { Suspense } from "react";

export default function AppLayout({ children }: LayoutProps) {
return (
<main className="flex-1 flex-col w-full">
<CompaniesHeader />
<Suspense fallback={<AppLoading />}>
<div className="mx-auto flex w-full max-w-5xl p-3">{children}</div>
</Suspense>
</main>
<>
<main className="flex-1 flex-col w-full">
<CompaniesHeader />
<Suspense fallback={<AppLoading />}>
<div className="mx-auto flex w-full max-w-5xl p-3">{children}</div>
</Suspense>
</main>
<Footer />
</>
);
}

Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/app/category/[category]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Footer from "@/components/Footer";
import { LayoutProps } from "@/lib/types";
import { Suspense } from "react";

export default function CategoryPageLayout({ children }: LayoutProps) {
return (
<>
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
<Footer />
</>
);
}
96 changes: 96 additions & 0 deletions apps/web/src/app/category/[category]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Breadcrumb } from "@/components/Breadcrumb";
import CompaniesList from "@/components/CompaniesList";
import {
APP_URL,
defaultMetadata,
defaultOpenGraphMetadata,
defaultTwitterMetadata,
} from "@/lib/metadata";
import { getParsedCompaniesData } from "@/lib/parser/companies";
import { NextParams } from "@/lib/types";
import { Metadata } from "next";
import { Suspense } from "react";

export async function generateMetadata({
params,
}: {
params: NextParams<{ category: string }>;
}): Promise<Metadata> {
const { category: categoryParam } = await params;

const category = decodeURIComponent(categoryParam);

const title = `${category} Companies | Tech Companies Portugal`;
const description = `Discover tech companies in the ${category} sector. Find job opportunities and connect with ${category} tech companies in Portugal.`;
const keywords = `${category} tech companies, ${category} software companies, IT employers ${category}, technology sector ${category}`;

const metadata = {
...defaultMetadata,
title,
description,
keywords,
alternates: {
canonical: `${APP_URL}/category/${category}`,
},
openGraph: {
...defaultOpenGraphMetadata,
title,
description,
url: `${APP_URL}/category/${category}`,
images: [`api/og?title=${title}&description=${description}`],
},
twitter: {
...defaultTwitterMetadata,
title,
description,
images: [`api/og?title=${title}&description=${description}`],
},
} satisfies Metadata;

return metadata;
}

export async function generateStaticParams() {
const { availableCategories } = await getParsedCompaniesData();

return availableCategories.map((category) => ({
category,
}));
}

export default async function CategoryPage({
params,
}: {
params: NextParams<{ category: string }>;
}) {
const { category: categoryParam } = await params;

const category = decodeURIComponent(categoryParam);

const { companies, updatedAtISODate } = await getParsedCompaniesData();

const filteredCompanies = companies.filter((company) =>
company.categories.includes(category),
);

return (
<section className="mx-auto flex w-full max-w-5xl p-3 relative flex-1">
<div className="flex flex-col gap-5 w-full">
<Breadcrumb
items={[
{ label: "Category", className: "text-muted-foreground" },
{ label: category },
]}
/>
<h1 className="text-2xl font-bold">{category} Companies</h1>
<Suspense>
<CompaniesList
allCompanies={filteredCompanies}
updatedAtISODate={updatedAtISODate}
allowSearchParams={false}
/>
</Suspense>
</div>
</section>
);
}
15 changes: 15 additions & 0 deletions apps/web/src/app/category/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { APP_URL } from "@/lib/metadata";
import { getParsedCompaniesData } from "@/lib/parser/companies";
import { MetadataRoute } from "next";

export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const { availableCategories, updatedAtISODate } =
await getParsedCompaniesData();

let categoriesRoutes = availableCategories.map((category) => ({
url: `${APP_URL}/category/${category}`,
lastModified: updatedAtISODate,
}));

return [...categoriesRoutes];
}
8 changes: 7 additions & 1 deletion apps/web/src/app/company/[slug]/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import SimpleFooter from "@/components/SimpleFooter";
import { Skeleton } from "@/components/ui/skeleton";
import { LayoutProps } from "@/lib/types";
import { Suspense } from "react";

export default function CompanyPageLayout({ children }: LayoutProps) {
return <Suspense fallback={<CompanyLoading />}>{children}</Suspense>;
return (
<>
<Suspense fallback={<CompanyLoading />}>{children}</Suspense>
<SimpleFooter />
</>
);
}

const CompanyLoading = () => {
Expand Down
12 changes: 8 additions & 4 deletions apps/web/src/app/company/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { notFound } from "next/navigation";
import { Metadata } from "next/types";

// force generation on demand for paths not known at build time
export const dynamicParams = true;
// export const dynamicParams = true;

export async function generateStaticParams() {
const companies = await getParsedCompaniesData();
Expand All @@ -29,7 +29,7 @@ export async function generateStaticParams() {
export async function generateMetadata({
params,
}: {
params: NextParams;
params: NextParams<{ slug: string }>;
}): Promise<Metadata | void> {
const { slug } = await params;

Expand All @@ -39,7 +39,7 @@ export async function generateMetadata({
return;
}

const title = `${company.name} - Leading Tech Company in Portugal | Explore Careers & More`;
const title = `${company.name} | Leading Tech Company in Portugal - Profile & Careers`;
const description = company.description;
const keywords = `${company.name}, Tech Company, Careers, Portugal`;

Expand Down Expand Up @@ -69,7 +69,11 @@ export async function generateMetadata({
return metadata;
}

export default async function CompanyPage({ params }: { params: NextParams }) {
export default async function CompanyPage({
params,
}: {
params: NextParams<{ slug: string }>;
}) {
const { slug } = await params;

const company = await getParsedCompanyBySlug(slug);
Expand Down
3 changes: 0 additions & 3 deletions apps/web/src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import Footer from "@/components/Footer";
import Navbar from "@/components/Navbar";
import DotPattern from "@/components/ui/dot-pattern";
import { GeistMono, GeistSans } from "@/lib/fonts";
Expand All @@ -14,7 +13,6 @@ import { AnalyticsProvider } from "@tech-companies-portugal/analytics/client";
import { Metadata, Viewport } from "next/types";
import { NuqsAdapter } from "nuqs/adapters/next/app";
import "./globals.css";

export const metadata: Metadata = {
...defaultMetadata,
twitter: {
Expand Down Expand Up @@ -51,7 +49,6 @@ export default function RootLayout({ children }: LayoutProps) {
<NuqsAdapter>
<Navbar />
{children}
<Footer />
<DotPattern
className={cn(
"[mask-image:radial-gradient(620px_circle_at_center,white,transparent)] fixed inset-0 -z-10",
Expand Down
12 changes: 12 additions & 0 deletions apps/web/src/app/location/[location]/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import Footer from "@/components/Footer";
import { LayoutProps } from "@/lib/types";
import { Suspense } from "react";

export default function LocationPageLayout({ children }: LayoutProps) {
return (
<>
<Suspense fallback={<div>Loading...</div>}>{children}</Suspense>
<Footer />
</>
);
}
97 changes: 97 additions & 0 deletions apps/web/src/app/location/[location]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { Breadcrumb } from "@/components/Breadcrumb";
import CompaniesList from "@/components/CompaniesList";
import {
APP_URL,
defaultMetadata,
defaultOpenGraphMetadata,
defaultTwitterMetadata,
} from "@/lib/metadata";
import { getParsedCompaniesData } from "@/lib/parser/companies";
import { NextParams } from "@/lib/types";
import { Metadata } from "next";
import { Suspense } from "react";

export async function generateMetadata({
params,
}: {
params: NextParams<{ location: string }>;
}): Promise<Metadata> {
const { location: locationParam } = await params;

const location = decodeURIComponent(locationParam);

const title = `Companies in ${location} | Tech Companies Portugal`;
const description = `Discover tech companies based in ${location} - Portugal. Find job opportunities and connect with tech companies in ${location} - Portugal.`;
const keywords = `tech companies in ${location}, Portugal tech jobs, ${location} software companies, IT employers ${location}`;

const metadata = {
...defaultMetadata,
title,
description,
keywords,
alternates: {
canonical: `${APP_URL}/location/${location}`,
},
openGraph: {
...defaultOpenGraphMetadata,
title,
description,
url: `${APP_URL}/location/${location}`,
images: [`api/og?title=${title}&description=${description}`],
},
twitter: {
...defaultTwitterMetadata,
title,
description,
images: [`api/og?title=${title}&description=${description}`],
},
} satisfies Metadata;

return metadata;
}

export async function generateStaticParams() {
const { availableLocations } = await getParsedCompaniesData();

return availableLocations.map((location) => ({
location,
}));
}

export default async function LocationPage({
params,
}: {
params: NextParams<{ location: string }>;
}) {
const { location: locationParam } = await params;

const location = decodeURIComponent(locationParam);

const { companies, updatedAtISODate } = await getParsedCompaniesData();

const filteredCompanies = companies.filter((company) =>
company.locations.includes(location),
);

return (
<section className="mx-auto flex w-full max-w-5xl p-3 relative">
<div className="flex flex-col gap-5 w-full">
<Breadcrumb
items={[
{ label: "Location", className: "text-muted-foreground" },
{ label: location },
]}
/>
<h1 className="text-2xl font-bold">Companies in {location}</h1>

<Suspense>
<CompaniesList
allCompanies={filteredCompanies}
updatedAtISODate={updatedAtISODate}
allowSearchParams={false}
/>
</Suspense>
</div>
</section>
);
}
Loading