@@ -191,22 +176,22 @@ const UserManagement = () => {
{user.name || user.username}
{"role" in user && user.role === "ADMIN" && (
-
+
Admin
)}
{"isBanned" in user && user.isBanned && (
-
+
Banned
)}
-
+
@{user.username} · {user.email}
{"createdAt" in user && user.createdAt && (
<> · Joined {getRelativeTime(user.createdAt)}>
@@ -220,7 +205,7 @@ const UserManagement = () => {
handleUnban(user.id)}
disabled={isUnbanning}
- className="flex items-center gap-1 rounded-lg border border-green-300 px-3 py-1.5 text-sm font-medium text-green-700 transition-colors hover:bg-green-50 disabled:opacity-50 dark:border-green-700 dark:text-green-400 dark:hover:bg-green-900/20"
+ className="secondary-button px-3 py-1.5 text-sm text-success disabled:opacity-50"
>
Unban
@@ -233,12 +218,12 @@ const UserManagement = () => {
placeholder="Reason for ban..."
value={banNote}
onChange={(e) => setBanNote(e.target.value)}
- className="w-48 rounded-lg border border-neutral-300 px-2 py-1 text-sm dark:border-neutral-600 dark:bg-neutral-700"
+ className="w-48 rounded-md border border-hairline bg-transparent px-2 py-1 text-sm text-fg placeholder-faint focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent"
/>
handleBan(user.id)}
disabled={isBanning}
- className="rounded-lg bg-red-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-red-700 disabled:opacity-50"
+ className="secondary-button px-3 py-1.5 text-sm text-danger disabled:opacity-50"
>
Confirm
@@ -247,7 +232,7 @@ const UserManagement = () => {
setSelectedUserId(null);
setBanNote("");
}}
- className="rounded-lg border border-neutral-300 px-3 py-1.5 text-sm font-medium text-neutral-600 hover:bg-neutral-100 dark:border-neutral-600 dark:text-neutral-400 dark:hover:bg-neutral-700"
+ className="secondary-button px-3 py-1.5 text-sm"
>
Cancel
@@ -255,7 +240,7 @@ const UserManagement = () => {
) : (
setSelectedUserId(user.id)}
- className="flex items-center gap-1 rounded-lg border border-red-300 px-3 py-1.5 text-sm font-medium text-red-700 transition-colors hover:bg-red-50 dark:border-red-700 dark:text-red-400 dark:hover:bg-red-900/20"
+ className="secondary-button px-3 py-1.5 text-sm text-danger"
>
Ban
@@ -267,13 +252,13 @@ const UserManagement = () => {
{/* Ban details if banned */}
{"banNote" in user && user.banNote && (
-
-
+
+
Ban reason: {" "}
{user.banNote}
{"bannedBy" in user && user.bannedBy && (
-
+
Banned by @{user.bannedBy.username}
)}
diff --git a/app/(app)/advertise/_client.tsx b/app/(app)/advertise/_client.tsx
deleted file mode 100644
index 4b69368cb..000000000
--- a/app/(app)/advertise/_client.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-"use client";
-
-import {
- HeroSection,
- MetricsSection,
- OfferingsSection,
- SocialProofSection,
- ContactSection,
-} from "@/components/Sponsorship";
-
-export function AdvertiseClient() {
- return (
- <>
-
-
-
-
-
- >
- );
-}
diff --git a/app/(app)/advertise/page.tsx b/app/(app)/advertise/page.tsx
deleted file mode 100644
index 6e3fd36a9..000000000
--- a/app/(app)/advertise/page.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { Metadata } from "next";
-import { AdvertiseClient } from "./_client";
-
-export const metadata: Metadata = {
- title: "Advertise with Codú - Reach Ireland's Developer Community",
- description:
- "Partner with Codú to reach 100,000+ monthly developer visits. Job postings, newsletter ads, event branding, and more. Connect with Ireland's largest web developer community.",
- openGraph: {
- title: "Advertise with Codú",
- description:
- "Connect your brand with Ireland's most engaged developer community. Sponsorship packages for job postings, newsletter advertising, and event branding.",
- },
-};
-
-export default function AdvertisePage() {
- return
;
-}
diff --git a/app/(app)/alpha/additional-details/_actions.ts b/app/(app)/alpha/additional-details/_actions.ts
deleted file mode 100644
index a5e6b4b12..000000000
--- a/app/(app)/alpha/additional-details/_actions.ts
+++ /dev/null
@@ -1,90 +0,0 @@
-"use server";
-
-import { getServerAuthSession } from "@/server/auth";
-import { redirect } from "next/navigation";
-
-import {
- type TypeSlideOneSchema,
- type TypeSlideTwoSchema,
- type TypeSlideThreeSchema,
- slideOneSchema,
- slideTwoSchema,
- slideThreeSchema,
-} from "@/schema/additionalUserDetails";
-import { db } from "@/server/db";
-import { user } from "@/server/db/schema";
-import { eq } from "drizzle-orm";
-
-export async function slideOneSubmitAction(dataInput: TypeSlideOneSchema) {
- const session = await getServerAuthSession();
- if (!session || !session.user) {
- redirect("/get-started");
- }
-
- try {
- const { name, username, location } = slideOneSchema.parse(dataInput);
-
- await db
- .update(user)
- .set({
- name,
- username,
- location,
- })
- .where(eq(user.id, session.user.id));
-
- return true;
- } catch {
- return false;
- }
-}
-
-export async function slideTwoSubmitAction(dataInput: TypeSlideTwoSchema) {
- const session = await getServerAuthSession();
- if (!session || !session.user) {
- redirect("/get-started");
- }
-
- try {
- const { dateOfBirth, gender } = slideTwoSchema.parse(dataInput);
-
- await db
- .update(user)
- .set({
- dateOfBirth,
- gender,
- })
- .where(eq(user.id, session.user.id));
-
- return true;
- } catch {
- return false;
- }
-}
-
-export async function slideThreeSubmitAction(dataInput: TypeSlideThreeSchema) {
- const session = await getServerAuthSession();
- if (!session || !session.user) {
- redirect("/get-started");
- }
-
- try {
- const { professionalOrStudent, course, jobTitle, levelOfStudy, workplace } =
- slideThreeSchema.parse(dataInput);
-
- await db
- .update(user)
- .set({
- professionalOrStudent,
- course,
- jobTitle,
- levelOfStudy,
- workplace,
- })
- .where(eq(user.id, session.user.id));
-
- return true;
- } catch {
- return false;
- }
-}
diff --git a/app/(app)/alpha/additional-details/_client.tsx b/app/(app)/alpha/additional-details/_client.tsx
deleted file mode 100644
index d559178fd..000000000
--- a/app/(app)/alpha/additional-details/_client.tsx
+++ /dev/null
@@ -1,591 +0,0 @@
-"use client";
-
-import React, { useEffect, useMemo, useState } from "react";
-import { redirect, useRouter, useSearchParams } from "next/navigation";
-import { useSession } from "next-auth/react";
-import {
- type TypeSlideOneSchema,
- type TypeSlideTwoSchema,
- type TypeSlideThreeSchema,
- slideOneSchema,
- slideTwoSchema,
- slideThreeSchema,
-} from "@/schema/additionalUserDetails";
-import { zodResolver } from "@hookform/resolvers/zod";
-import { useForm } from "react-hook-form";
-
-import { toast } from "sonner";
-import {
- professionalOrStudentOptions,
- genderOptions,
- locationOptions,
- levelOfStudyOptions,
- monthsOptions,
-} from "@/app/(app)/alpha/additional-details/selectOptions";
-import {
- slideOneSubmitAction,
- slideThreeSubmitAction,
- slideTwoSubmitAction,
-} from "./_actions";
-import {
- ErrorMessage,
- Field,
- Fieldset,
- Label,
- Legend,
-} from "@/components/ui-components/fieldset";
-import { Input } from "@/components/ui-components/input";
-import { Select } from "@/components/ui-components/select";
-import { Button } from "@/components/ui-components/button";
-import { Heading, Subheading } from "@/components/ui-components/heading";
-import { Divider } from "@/components/ui-components/divider";
-
-type UserDetails = {
- username: string;
- name: string;
- gender: string;
- dateOfBirth: string;
- location: string;
- professionalOrStudent: string;
- course: string;
- levelOfStudy: string;
- jobTitle: string;
- workplace: string;
-};
-
-export default function AdditionalSignUpDetails({
- details,
-}: {
- details: UserDetails;
-}) {
- const session = useSession();
- const searchParams = useSearchParams();
-
- const {
- name,
- username,
- location,
- dateOfBirth,
- gender,
- professionalOrStudent,
- } = details;
-
- let slide: number;
- if (searchParams.get("slide")) {
- slide = Number(searchParams.get("slide"));
- } else if (!name || !username || !location) {
- slide = 1;
- } else if (!dateOfBirth || !gender) {
- slide = 2;
- } else if (!professionalOrStudent) {
- slide = 3;
- } else {
- return redirect("/settings");
- }
-
- if (!session) {
- return redirect("/get-started");
- }
- return (
-
-
-
- {slide === 1 && }
- {slide === 2 && }
- {slide === 3 && }
-
- );
-}
-
-function SlideOne({ details }: { details: UserDetails }) {
- const router = useRouter();
-
- const { username, name, location } = details;
-
- const {
- register,
- handleSubmit,
- formState: { errors, isSubmitting },
- } = useForm
({
- resolver: zodResolver(slideOneSchema),
- defaultValues: { username, name, location },
- });
-
- const onFormSubmit = async (data: TypeSlideOneSchema) => {
- try {
- const isSuccess = await slideOneSubmitAction(data);
- if (isSuccess) {
- toast.success("Saved");
- router.push(`?slide=${2}`, { scroll: false });
- } else {
- toast.error("Error, saving was unsuccessful.");
- }
- } catch {
- toast.error("An unexpected error occurred.");
- }
- };
-
- return (
-
- );
-}
-
-function SlideTwo({ details }: { details: UserDetails }) {
- const router = useRouter();
- const { dateOfBirth, gender } = details;
-
- const {
- register,
- handleSubmit,
- setValue,
- formState: { errors, isSubmitting },
- } = useForm({
- resolver: zodResolver(slideTwoSchema),
- defaultValues: { dateOfBirth, gender },
- });
-
- const parsedDateOfBirth = dateOfBirth ? new Date(dateOfBirth) : null;
- const [year, setYear] = useState(
- parsedDateOfBirth?.getFullYear(),
- );
- const [month, setMonth] = useState(
- parsedDateOfBirth?.getMonth(),
- );
- const [day, setDay] = useState(
- parsedDateOfBirth?.getDate(),
- );
-
- // Compute days in month directly from year/month (no state needed)
- const listOfDaysInSelectedMonth = useMemo(() => {
- if (year && month !== undefined) {
- // Returns the last day of the month, by creating a date with day 0 of the following month.
- const numberOfDaysInMonth = new Date(year, month + 1, 0).getDate();
- return Array.from(
- { length: numberOfDaysInMonth },
- (_, index) => index + 1,
- );
- }
- return [0];
- }, [year, month]);
-
- // Update the date object when year, month or day change
- useEffect(() => {
- if (year && month !== undefined && day) {
- let selectedDate: Date;
-
- if (new Date(year, month, day).getDate() < day) {
- // If user switches to a month in which selected day does not exist
- // ie from 30/1 to feb(only 28 days), then set day to the 1st
- selectedDate = new Date(year, month, 1);
- } else {
- selectedDate = new Date(year, month, day);
- }
- setValue("dateOfBirth", selectedDate.toISOString());
- }
- }, [year, month, day, setValue]);
-
- const startYearAgeDropdown = 1950;
- const endYearAgeDropdown = 2010;
- const years = Array.from(
- { length: endYearAgeDropdown - startYearAgeDropdown + 1 },
- (_, index) => startYearAgeDropdown + index,
- ).reverse();
-
- const onFormSubmit = async (data: TypeSlideTwoSchema) => {
- try {
- const isSuccess = await slideTwoSubmitAction(data);
-
- if (isSuccess) {
- toast.success("Saved");
- router.push(`?slide=${3}`, { scroll: false });
- } else {
- toast.error("Error, saving was unsuccessful.");
- }
- } catch {
- toast.error("An unexpected error occurred.");
- }
- };
-
- return (
-
- );
-}
-
-function SlideThree({ details }: { details: UserDetails }) {
- const router = useRouter();
-
- const { professionalOrStudent, workplace, jobTitle, course, levelOfStudy } =
- details;
-
- const {
- register,
- handleSubmit,
- trigger,
- watch,
- getValues,
- formState: { errors, isSubmitting },
- } = useForm({
- resolver: zodResolver(slideThreeSchema),
- defaultValues: {
- professionalOrStudent,
- workplace,
- jobTitle,
- course,
- levelOfStudy,
- },
- });
-
- watch("professionalOrStudent");
-
- const onFormSubmit = async (data: TypeSlideThreeSchema) => {
- let isError = await trigger(["professionalOrStudent"]);
- const professionalOrStudent = getValues("professionalOrStudent");
-
- if (isError && professionalOrStudent === "Working professional") {
- isError = await trigger(["workplace", "jobTitle"]);
- }
-
- if (isError && professionalOrStudent === "Current student") {
- isError = await trigger(["levelOfStudy", "course"]);
- }
-
- if (isError) {
- try {
- const isSuccess = await slideThreeSubmitAction(data);
- if (isSuccess) {
- toast.success("Saved");
- router.push(`/`, { scroll: false });
- } else {
- toast.error("Error, saving was unsuccessful.");
- }
- } catch {
- toast.error("An unexpected error occurred.");
- }
- }
- };
-
- return (
-
- );
-}
-
-function SignupProgressBar({ currentSlide }: { currentSlide: number }) {
- const progressPercentage = (100 / 4) * currentSlide;
-
- return (
-
-
-
{progressPercentage}%
-
- );
-}
diff --git a/app/(app)/alpha/additional-details/page.tsx b/app/(app)/alpha/additional-details/page.tsx
deleted file mode 100644
index dfe5d1beb..000000000
--- a/app/(app)/alpha/additional-details/page.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { redirect } from "next/navigation";
-import Content from "./_client";
-import { getServerAuthSession } from "@/server/auth";
-import { db } from "@/server/db";
-
-export default async function Page() {
- const session = await getServerAuthSession();
- if (!session || !session.user) {
- return redirect("/get-started");
- }
-
- const userId = session.user.id;
- const details = await db.query.user.findFirst({
- columns: {
- username: true,
- name: true,
- gender: true,
- dateOfBirth: true,
- location: true,
- professionalOrStudent: true,
- course: true,
- levelOfStudy: true,
- jobTitle: true,
- workplace: true,
- },
- where: (user, { eq }) => eq(user.id, userId),
- });
-
- const detailsWithNullsRemoved = {
- username: details?.username || "",
- name: details?.name || "",
- gender: details?.gender || "",
- dateOfBirth: details?.dateOfBirth || "",
- location: details?.location || "",
- professionalOrStudent: details?.professionalOrStudent || "",
- course: details?.course || "",
- levelOfStudy: details?.levelOfStudy || "",
- jobTitle: details?.jobTitle || "",
- workplace: details?.workplace || "",
- };
-
- return ;
-}
diff --git a/app/(app)/alpha/additional-details/selectOptions.ts b/app/(app)/alpha/additional-details/selectOptions.ts
deleted file mode 100644
index 552f76761..000000000
--- a/app/(app)/alpha/additional-details/selectOptions.ts
+++ /dev/null
@@ -1,283 +0,0 @@
-export const genderOptions = ["Male", "Female", "Other", "Rather not say"];
-
-export const professionalOrStudentOptions = [
- "Current student",
- "Working professional",
- "None of the above",
-];
-
-export const levelOfStudyOptions = [
- "Elementry / Middle School / Primary School",
- "High School / Secondary School",
- "Univeristy (Undergraduate)",
- "Univeristy (Masters / Doctoral)",
- "Vocational / Code school",
- "Not currently a student",
- "Other",
-];
-
-export const monthsOptions = [
- "January",
- "February",
- "March",
- "April",
- "May",
- "June",
- "July",
- "August",
- "September",
- "October",
- "November",
- "December",
-];
-export const locationOptions = [
- "Afghanistan",
- "Albania",
- "Algeria",
- "American Samoa",
- "Andorra",
- "Angola",
- "Anguilla",
- "Antarctica",
- "Antigua and Barbuda",
- "Argentina",
- "Armenia",
- "Aruba",
- "Australia",
- "Austria",
- "Azerbaijan",
- "Bahamas (the)",
- "Bahrain",
- "Bangladesh",
- "Barbados",
- "Belarus",
- "Belgium",
- "Belize",
- "Benin",
- "Bermuda",
- "Bhutan",
- "Bolivia (Plurinational State of)",
- "Bonaire, Sint Eustatius and Saba",
- "Bosnia and Herzegovina",
- "Botswana",
- "Bouvet Island",
- "Brazil",
- "British Indian Ocean Territory (the)",
- "Brunei Darussalam",
- "Bulgaria",
- "Burkina Faso",
- "Burundi",
- "Cabo Verde",
- "Cambodia",
- "Cameroon",
- "Canada",
- "Cayman Islands (the)",
- "Central African Republic (the)",
- "Chad",
- "Chile",
- "China",
- "Christmas Island",
- "Cocos (Keeling) Islands (the)",
- "Colombia",
- "Comoros (the)",
- "Congo (the Democratic Republic of the)",
- "Congo (the)",
- "Cook Islands (the)",
- "Costa Rica",
- "Croatia",
- "Cuba",
- "Curaçao",
- "Cyprus",
- "Czechia",
- "Côte d'Ivoire",
- "Denmark",
- "Djibouti",
- "Dominica",
- "Dominican Republic (the)",
- "Ecuador",
- "Egypt",
- "El Salvador",
- "Equatorial Guinea",
- "Eritrea",
- "Estonia",
- "Eswatini",
- "Ethiopia",
- "Falkland Islands (the) [Malvinas]",
- "Faroe Islands (the)",
- "Fiji",
- "Finland",
- "France",
- "French Guiana",
- "French Polynesia",
- "French Southern Territories (the)",
- "Gabon",
- "Gambia (the)",
- "Georgia",
- "Germany",
- "Ghana",
- "Gibraltar",
- "Greece",
- "Greenland",
- "Grenada",
- "Guadeloupe",
- "Guam",
- "Guatemala",
- "Guernsey",
- "Guinea",
- "Guinea-Bissau",
- "Guyana",
- "Haiti",
- "Heard Island and McDonald Islands",
- "Holy See (the)",
- "Honduras",
- "Hong Kong",
- "Hungary",
- "Iceland",
- "India",
- "Indonesia",
- "Iran (Islamic Republic of)",
- "Iraq",
- "Ireland",
- "Isle of Man",
- "Israel",
- "Italy",
- "Jamaica",
- "Japan",
- "Jersey",
- "Jordan",
- "Kazakhstan",
- "Kenya",
- "Kiribati",
- "Korea (the Democratic People's Republic of)",
- "Korea (the Republic of)",
- "Kuwait",
- "Kyrgyzstan",
- "Lao People's Democratic Republic (the)",
- "Latvia",
- "Lebanon",
- "Lesotho",
- "Liberia",
- "Libya",
- "Liechtenstein",
- "Lithuania",
- "Luxembourg",
- "Macao",
- "Madagascar",
- "Malawi",
- "Malaysia",
- "Maldives",
- "Mali",
- "Malta",
- "Marshall Islands (the)",
- "Martinique",
- "Mauritania",
- "Mauritius",
- "Mayotte",
- "Mexico",
- "Micronesia (Federated States of)",
- "Moldova (the Republic of)",
- "Monaco",
- "Mongolia",
- "Montenegro",
- "Montserrat",
- "Morocco",
- "Mozambique",
- "Myanmar",
- "Namibia",
- "Nauru",
- "Nepal",
- "Netherlands (the)",
- "New Caledonia",
- "New Zealand",
- "Nicaragua",
- "Niger (the)",
- "Nigeria",
- "Niue",
- "Norfolk Island",
- "Northern Mariana Islands (the)",
- "Norway",
- "Oman",
- "Pakistan",
- "Palau",
- "Palestine, State of",
- "Panama",
- "Papua New Guinea",
- "Paraguay",
- "Peru",
- "Philippines (the)",
- "Pitcairn",
- "Poland",
- "Portugal",
- "Puerto Rico",
- "Qatar",
- "Republic of North Macedonia",
- "Romania",
- "Russian Federation (the)",
- "Rwanda",
- "Réunion",
- "Saint Barthélemy",
- "Saint Helena, Ascension and Tristan da Cunha",
- "Saint Kitts and Nevis",
- "Saint Lucia",
- "Saint Martin (French part)",
- "Saint Pierre and Miquelon",
- "Saint Vincent and the Grenadines",
- "Samoa",
- "San Marino",
- "Sao Tome and Principe",
- "Saudi Arabia",
- "Senegal",
- "Serbia",
- "Seychelles",
- "Sierra Leone",
- "Singapore",
- "Sint Maarten (Dutch part)",
- "Slovakia",
- "Slovenia",
- "Solomon Islands",
- "Somalia",
- "South Africa",
- "South Georgia and the South Sandwich Islands",
- "South Sudan",
- "Spain",
- "Sri Lanka",
- "Sudan (the)",
- "Suriname",
- "Svalbard and Jan Mayen",
- "Sweden",
- "Switzerland",
- "Syrian Arab Republic",
- "Taiwan",
- "Tajikistan",
- "Tanzania, United Republic of",
- "Thailand",
- "Timor-Leste",
- "Togo",
- "Tokelau",
- "Tonga",
- "Trinidad and Tobago",
- "Tunisia",
- "Turkey",
- "Turkmenistan",
- "Turks and Caicos Islands (the)",
- "Tuvalu",
- "Uganda",
- "Ukraine",
- "United Arab Emirates (the)",
- "United Kingdom of Great Britain and Northern Ireland (the)",
- "United States Minor Outlying Islands (the)",
- "United States of America (the)",
- "Uruguay",
- "Uzbekistan",
- "Vanuatu",
- "Venezuela (Bolivarian Republic of)",
- "Viet Nam",
- "Virgin Islands (British)",
- "Virgin Islands (U.S.)",
- "Wallis and Futuna",
- "Western Sahara",
- "Yemen",
- "Zambia",
- "Zimbabwe",
- "Åland Islands",
-];
diff --git a/app/(app)/alpha/courses/page.tsx b/app/(app)/alpha/courses/page.tsx
deleted file mode 100644
index 05ee6edcb..000000000
--- a/app/(app)/alpha/courses/page.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-// @TODO add appropriate metadata
-
-import CoursePreview from "@/components/Course/CoursePreview";
-
-/**
- * Feel free to update this interface when the Course model
- * has been clearly defined in the Drizzle schema.
- */
-
-const Courses = () => {
- const courses = [
- {
- name: "Introduction to Web Development",
- description:
- "Discover the magic of web development in our beginner-friendly course! Master HTML for structure, CSS for style, and JavaScript for interactivity. Learn from experts, tackle hands-on projects, and build a strong foundation to create stunning, responsive websites. Unleash your creativity and join us today! Perfect for your first steps into a programming career or looking to refresh their fundamentals.",
- thumbnail:
- "https://images.unsplash.com/photo-1612758272676-dec3a658080e?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1770&q=80",
- },
- ];
-
- return (
- <>
-
-
-
- Courses
-
-
-
-
- {courses.map(({ name, description, thumbnail }) => (
-
- ))}
-
-
- >
- );
-};
-
-export default Courses;
diff --git a/app/(app)/alpha/courses/web-development/page.tsx b/app/(app)/alpha/courses/web-development/page.tsx
deleted file mode 100644
index 64c1ecc08..000000000
--- a/app/(app)/alpha/courses/web-development/page.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-// @TODO add appropriate metadata
-
-import {
- CourseContens,
- type IContent,
-} from "@/components/Course/CourseContents";
-import { CircularProgressBar } from "@/components/Course/CircularProgressBar";
-
-// ! This is hardcoded and needs to be dynamic
-// ! I assume the route also should like [course-name].tsx , not web-development.tsx. Keep in mind it is hardcoded for now!
-
-const contentsMock: IContent[] = [
- { title: "How to use this site", completed: true },
- { title: "About this course" },
- { title: "Getting help" },
- { title: "Join the community" },
-];
-
-const WebDevelopmentCourse = () => {
- return (
- <>
-
-
-
- Introduction to Web Development
-
-
-
-
-
-
- An introduction to web development designed to take you from your
- first steps in coding to building an interactive game.
-
- {/* Course Contents */}
-
-
-
-
-
-
-
-
- Your progress
-
-
-
- Introduction to Web Development
-
-
- {/* Progress bar */}
-
-
-
-
-
-
- >
- );
-};
-
-export default WebDevelopmentCourse;
diff --git a/app/(app)/alpha/layout.tsx b/app/(app)/alpha/layout.tsx
deleted file mode 100644
index 0c56d8627..000000000
--- a/app/(app)/alpha/layout.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import React from "react";
-import { notFound } from "next/navigation";
-
-export const metadata = {
- title: "🚨 WIP - Things will break",
- robots: {
- follow: false,
- index: false,
- },
-};
-
-export default function Alpha({ children }: { children: React.ReactNode }) {
- if (process.env.ALPHA || process.env.NODE_ENV === "development") {
- return <>{children}>;
- }
- notFound();
-}
diff --git a/app/(app)/alpha/new/[[...postIdArr]]/_client.tsx b/app/(app)/alpha/new/[[...postIdArr]]/_client.tsx
deleted file mode 100644
index e262d7097..000000000
--- a/app/(app)/alpha/new/[[...postIdArr]]/_client.tsx
+++ /dev/null
@@ -1,276 +0,0 @@
-"use client";
-import { redirect, useParams } from "next/navigation";
-import React, { useEffect, Fragment } from "react";
-import { Controller } from "react-hook-form";
-import {
- Disclosure,
- DisclosureButton,
- DisclosurePanel,
- Transition,
-} from "@headlessui/react";
-import { ChevronUpIcon } from "@heroicons/react/20/solid";
-import Editor from "@/components/editor/editor";
-import useCreatePage from "@/hooks/useCreatePage";
-import { usePrompt } from "@/components/PromptService";
-
-const Create = () => {
- const params = useParams();
- const postId = params?.postIdArr?.[0] || "";
-
- const {
- tags,
- tagValue,
- savedTime,
- open,
- setOpen,
- hasUnsavedChanges,
- handleSubmit,
- register,
- control,
- savePost,
- isDisabled,
- onSubmit,
- onChange,
- onDelete,
- onKeyDown,
- handleOpenDialog,
- data,
- hasLoadingState,
- dataStatus,
- title,
- body,
- saveStatus,
- } = useCreatePage({ postId });
-
- const { unsavedChanges: _unsaved, setUnsavedChanges: _setUnsaved } =
- usePrompt();
-
- useEffect(() => {
- _setUnsaved(hasUnsavedChanges);
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [hasUnsavedChanges]);
-
- return (
- <>
-
- >
- );
-};
-
-export default Create;
diff --git a/app/(app)/alpha/new/[[...postIdArr]]/page.tsx b/app/(app)/alpha/new/[[...postIdArr]]/page.tsx
deleted file mode 100644
index 2c40cf8e6..000000000
--- a/app/(app)/alpha/new/[[...postIdArr]]/page.tsx
+++ /dev/null
@@ -1,14 +0,0 @@
-import { redirect } from "next/navigation";
-import Content from "./_client";
-import { getServerAuthSession } from "@/server/auth";
-
-// @TODO - Maybe add Metadata for this page
-
-export default async function Page() {
- const session = await getServerAuthSession();
- if (!session) {
- redirect("/get-started");
- }
-
- return ;
-}
diff --git a/app/(app)/articles/_client.tsx b/app/(app)/articles/_client.tsx
deleted file mode 100644
index ed84021fe..000000000
--- a/app/(app)/articles/_client.tsx
+++ /dev/null
@@ -1,547 +0,0 @@
-"use client";
-
-import { Fragment, useEffect, useState } from "react";
-import { TagIcon } from "@heroicons/react/20/solid";
-import {
- ChevronUpIcon,
- ChevronDownIcon,
- BookmarkIcon,
- ChatBubbleLeftIcon,
- ShareIcon,
- EllipsisHorizontalIcon,
-} from "@heroicons/react/20/solid";
-import { BookmarkIcon as BookmarkOutlineIcon } from "@heroicons/react/24/outline";
-import {
- Menu,
- MenuButton,
- MenuItem,
- MenuItems,
- Transition,
-} from "@headlessui/react";
-import { useInView } from "react-intersection-observer";
-import { useSearchParams, useRouter } from "next/navigation";
-import Link from "next/link";
-import { api } from "@/server/trpc/react";
-import SideBarSavedPosts from "@/components/SideBar/SideBarSavedPosts";
-import { useSession, signIn } from "next-auth/react";
-import { getCamelCaseFromLower } from "@/utils/utils";
-import PopularTagsLoading from "@/components/PopularTags/PopularTagsLoading";
-import NewsletterCTA from "@/components/NewsletterCTA/NewsletterCTA";
-import { toast } from "sonner";
-import * as Sentry from "@sentry/nextjs";
-import { FeedFilters } from "@/components/Feed";
-import { useReportModal } from "@/components/ReportModal/ReportModal";
-
-// Get relative time string
-const getRelativeTime = (dateStr: string): string => {
- const now = new Date();
- const date = new Date(dateStr);
- const diffMs = now.getTime() - date.getTime();
- const diffMins = Math.floor(diffMs / 60000);
- const diffHours = Math.floor(diffMs / 3600000);
- const diffDays = Math.floor(diffMs / 86400000);
-
- if (diffMins < 1) return "just now";
- if (diffMins < 60) return `${diffMins}m ago`;
- if (diffHours < 24) return `${diffHours}h ago`;
- if (diffDays < 7) return `${diffDays}d ago`;
- return date.toLocaleDateString("en-US", { month: "short", day: "numeric" });
-};
-
-// Article card component with voting
-type ArticleCardProps = {
- id: string;
- slug: string;
- title: string;
- excerpt: string | null;
- name: string;
- username: string;
- image: string;
- date: string;
- readTime: number;
- upvotes: number;
- downvotes: number;
- userVote: "up" | "down" | null;
- isBookmarked: boolean;
- discussionCount?: number;
-};
-
-const ArticleCard = ({
- id,
- slug,
- title,
- excerpt,
- name,
- username,
- image,
- date,
- readTime,
- upvotes,
- downvotes,
- userVote: initialUserVote,
- isBookmarked: initialBookmarked,
- discussionCount = 0,
-}: ArticleCardProps) => {
- const { data: session } = useSession();
- const utils = api.useUtils();
- const [userVote, setUserVote] = useState(initialUserVote);
- const [votes, setVotes] = useState({ upvotes, downvotes });
- const [isBookmarked, setIsBookmarked] = useState(initialBookmarked);
- const { openReport } = useReportModal();
-
- const { mutate: vote, status: voteStatus } = api.post.vote.useMutation({
- onMutate: async ({ voteType }) => {
- const oldVote = userVote;
- setUserVote(voteType);
-
- setVotes((prev) => {
- let newUpvotes = prev.upvotes;
- let newDownvotes = prev.downvotes;
-
- if (oldVote === "up") newUpvotes--;
- if (oldVote === "down") newDownvotes--;
-
- if (voteType === "up") newUpvotes++;
- if (voteType === "down") newDownvotes++;
-
- return { upvotes: newUpvotes, downvotes: newDownvotes };
- });
- },
- onError: (error) => {
- setUserVote(initialUserVote);
- setVotes({ upvotes, downvotes });
- toast.error("Failed to update vote");
- Sentry.captureException(error);
- },
- onSettled: () => {
- utils.post.published.invalidate();
- },
- });
-
- const { mutate: bookmark, status: bookmarkStatus } =
- api.post.bookmark.useMutation({
- onMutate: async ({ setBookmarked }) => {
- setIsBookmarked(setBookmarked);
- },
- onError: (error) => {
- setIsBookmarked(initialBookmarked);
- toast.error("Failed to update bookmark");
- Sentry.captureException(error);
- },
- onSettled: () => {
- utils.post.myBookmarks.invalidate();
- },
- });
-
- const handleVote = (voteType: "up" | "down" | null) => {
- if (!session) {
- signIn();
- return;
- }
- vote({ postId: id, voteType });
- };
-
- const handleBookmark = () => {
- if (!session) {
- signIn();
- return;
- }
- bookmark({ postId: id, setBookmarked: !isBookmarked });
- };
-
- const handleShare = async () => {
- const shareUrl = `${window.location.origin}/${username}/${slug}`;
- try {
- await navigator.clipboard.writeText(shareUrl);
- toast.success("Link copied to clipboard");
- } catch {
- toast.error("Failed to copy link");
- }
- };
-
- const handleReport = () => {
- if (!session) {
- signIn();
- return;
- }
- openReport("post", id);
- };
-
- const relativeTime = getRelativeTime(date);
- const score = votes.upvotes - votes.downvotes;
-
- return (
-
- {/* Header row - author and metadata */}
-
-
-
-
{name}
-
-
·
-
{relativeTime}
- {readTime > 0 && (
- <>
-
·
-
{readTime} min read
- >
- )}
-
-
- {/* Title */}
-
-
- {title}
-
-
-
- {/* Excerpt */}
- {excerpt && (
-
- {excerpt}
-
- )}
-
- {/* Action bar */}
-
- {/* Vote buttons */}
-
- handleVote(userVote === "up" ? null : "up")}
- disabled={voteStatus === "pending"}
- className={`rounded-l-full p-1 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-neutral-800 ${
- userVote === "up"
- ? "text-green-500"
- : "text-neutral-400 dark:text-neutral-500"
- }`}
- aria-label="Upvote"
- >
-
-
- 0
- ? "text-green-500"
- : score < 0
- ? "text-red-500"
- : "text-neutral-400 dark:text-neutral-500"
- }`}
- >
- {score}
-
- handleVote(userVote === "down" ? null : "down")}
- disabled={voteStatus === "pending"}
- className={`rounded-r-full p-1 transition-colors hover:bg-neutral-100 disabled:cursor-not-allowed disabled:opacity-50 dark:hover:bg-neutral-800 ${
- userVote === "down"
- ? "text-red-500"
- : "text-neutral-400 dark:text-neutral-500"
- }`}
- aria-label="Downvote"
- >
-
-
-
-
- {/* Comments button */}
-
-
-
{discussionCount}
-
-
- {/* Save button */}
-
- {isBookmarked ? (
-
- ) : (
-
- )}
-
- {isBookmarked ? "Saved" : "Save"}
-
-
-
- {/* Share button */}
-
-
- Share
-
-
- {/* Triple-dot menu */}
-
-
- More options
-
-
-
-
-
-
- Copy link
-
-
-
-
- Report
-
-
-
-
-
-
-
- );
-};
-
-// Loading skeleton
-const ArticleCardLoading = () => (
-
-
-
-
-
-
-);
-
-// Sort option types - unified between feed and articles
-type UISortOption = "recent" | "trending" | "popular";
-type APISortOption = "newest" | "oldest" | "top" | "trending";
-
-// Map UI sort to API sort
-const sortUIToAPI: Record = {
- recent: "newest",
- trending: "trending",
- popular: "top",
-};
-
-const validUISorts: UISortOption[] = ["recent", "trending", "popular"];
-
-const ArticlesPage = () => {
- const searchParams = useSearchParams();
- const router = useRouter();
- const { data: session } = useSession();
- const sortParam = searchParams?.get("sort");
- const dirtyTag = searchParams?.get("tag");
-
- const tag = typeof dirtyTag === "string" ? dirtyTag : null;
-
- // Get UI sort from URL param
- const uiSort: UISortOption = validUISorts.includes(sortParam as UISortOption)
- ? (sortParam as UISortOption)
- : "recent";
-
- // Convert to API sort for the query
- const apiSort = sortUIToAPI[uiSort];
-
- const { status, data, isFetchingNextPage, fetchNextPage, hasNextPage } =
- api.post.published.useInfiniteQuery(
- {
- limit: 15,
- sort: apiSort,
- tag,
- },
- {
- getNextPageParam: (lastPage) => lastPage.nextCursor,
- },
- );
-
- const { status: tagsStatus, data: tagsData } = api.tag.get.useQuery();
-
- const { ref, inView } = useInView();
-
- useEffect(() => {
- if (inView && hasNextPage) {
- fetchNextPage();
- }
- }, [inView, hasNextPage, fetchNextPage]);
-
- // Handle filter changes
- const handleSortChange = (newSort: UISortOption) => {
- const params = new URLSearchParams();
- if (newSort !== "recent") params.set("sort", newSort);
- if (tag) params.set("tag", tag);
- const queryString = params.toString();
- router.push(`/articles${queryString ? `?${queryString}` : ""}`);
- };
-
- const handleTagChange = (newTag: string | null) => {
- const params = new URLSearchParams();
- if (uiSort !== "recent") params.set("sort", uiSort);
- if (newTag) params.set("tag", newTag);
- const queryString = params.toString();
- router.push(`/articles${queryString ? `?${queryString}` : ""}`);
- };
-
- // Get tags list for the filter dropdown
- const tagsList = tagsData?.data.map((t) => t.title.toLowerCase()) || [];
-
- return (
- <>
-
-
-
- {typeof tag === "string" ? (
-
-
- {getCamelCaseFromLower(tag)}
-
- ) : (
- "Articles"
- )}
-
-
-
-
-
-
- {status === "error" && (
-
- Something went wrong... Please refresh your page.
-
- )}
- {status === "pending" &&
- Array.from({ length: 7 }, (_, i) => (
-
- ))}
- {status === "success" &&
- data.pages.map((page) => {
- return (
-
- {page.posts.map(
- ({
- slug,
- title,
- excerpt,
- user,
- published,
- readTimeMins,
- id,
- currentUserBookmarkedPost,
- upvotes,
- downvotes,
- userVote,
- }) => {
- if (!published) return null;
- return (
-
- );
- },
- )}
-
- );
- })}
- {status === "success" && !data.pages[0].posts.length && (
-
-
- No articles found
-
-
- Check back later for new content.
-
-
- )}
- {isFetchingNextPage && }
-
- intersection observer marker
-
-
-
-
-
-
- Popular topics
-
-
- {tagsStatus === "pending" &&
}
- {tagsStatus === "success" &&
- tagsData.data.map(({ title }) => (
-
- {getCamelCaseFromLower(title)}
-
- ))}
-
- {session && (
-
-
-
- )}
-
-
-
- >
- );
-};
-
-export default ArticlesPage;
diff --git a/app/(app)/articles/page.tsx b/app/(app)/articles/page.tsx
index d26d6e343..c34b76267 100644
--- a/app/(app)/articles/page.tsx
+++ b/app/(app)/articles/page.tsx
@@ -1,7 +1,7 @@
-import { redirect } from "next/navigation";
+import { permanentRedirect } from "next/navigation";
-// Redirect /articles to /feed?type=article
-// The unified feed now handles all content types with filtering
+// Permanently redirect the legacy /articles index to the unified feed —
+// /articles carries years of equity that should consolidate onto "/".
export default function Page() {
- redirect("/feed?type=article");
+ permanentRedirect("/?type=article");
}
diff --git a/app/(app)/auth/page.tsx b/app/(app)/auth/page.tsx
index c925dfa3c..403ea6818 100644
--- a/app/(app)/auth/page.tsx
+++ b/app/(app)/auth/page.tsx
@@ -27,7 +27,7 @@ export const PostAuthPage = (content: {
if (!mounted) return null;
return (
-
+
Codú
@@ -46,15 +46,15 @@ export const PostAuthPage = (content: {
-
+
{content.heading}{" "}
-