diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..f23bbc6 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Optional for the public site. Copy this file to .env.local when needed. + +# Neon PostgreSQL connection string; leave blank until database features are added. +# Example: postgresql://USER:PASSWORD@HOST/DATABASE?sslmode=require +DATABASE_URL= + +# Better Auth is installed for future use; no auth routes are configured yet. +# Generate a secret with: openssl rand -base64 32 +BETTER_AUTH_SECRET= +BETTER_AUTH_URL=http://localhost:3000 + +# Resend API key and a sender address on your verified domain. +# Local email previews do not need these values. +RESEND_API_KEY= +RESEND_FROM_EMAIL= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..614a775 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: Next.js checks + +on: + push: + branches: [main] + pull_request: + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + - run: pnpm install --frozen-lockfile + - run: pnpm typecheck + - run: pnpm lint + - run: pnpm build diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml deleted file mode 100644 index d7b54f7..0000000 --- a/.github/workflows/docker-publish.yml +++ /dev/null @@ -1,87 +0,0 @@ -name: Docker - -on: - push: - branches: [ "main" ] - -env: - # Use docker.io for Docker Hub if empty - REGISTRY: ghcr.io - # github.repository as / - IMAGE_NAME: ${{ github.repository }} - - -jobs: - build: - - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - # This is used to complete the identity challenge - # with sigstore/fulcio when running outside of PRs. - id-token: write - - steps: - - name: Checkout repository - uses: actions/checkout@v3 - - # Install the cosign tool except on PR - # https://github.com/sigstore/cosign-installer - - name: Install cosign - if: github.event_name != 'pull_request' - uses: sigstore/cosign-installer@f3c664df7af409cb4873aa5068053ba9d61a57b6 #v2.6.0 - with: - cosign-release: 'v1.13.1' - - - name: Change directory - run: cd backend/ - - # Workaround: https://github.com/docker/build-push-action/issues/461 - - name: Setup Docker buildx - uses: docker/setup-buildx-action@79abd3f86f79a9d68a23c75a09a9a85889262adf - - # Login against a Docker registry except on PR - # https://github.com/docker/login-action - - name: Log into registry ${{ env.REGISTRY }} - if: github.event_name != 'pull_request' - uses: docker/login-action@28218f9b04b4f3f62068d7b6ce6ca5b26e35336c - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - # Extract metadata (tags, labels) for Docker - # https://github.com/docker/metadata-action - - name: Extract Docker metadata - id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - # Build and push Docker image with Buildx (don't push on PR) - # https://github.com/docker/build-push-action - - name: Build and push Docker image - id: build-and-push - uses: docker/build-push-action@ac9327eae2b366085ac7f6a2d02df8aa8ead720a - with: - context: backend/ - push: ${{ github.event_name != 'pull_request' }} - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha - cache-to: type=gha,mode=max - - - # Sign the resulting Docker image digest except on PRs. - # This will only write to the public Rekor transparency log when the Docker - # repository is public to avoid leaking data. If you would like to publish - # transparency data even for private images, pass --force to cosign below. - # https://github.com/sigstore/cosign - - name: Sign the published Docker image - if: ${{ github.event_name != 'pull_request' }} - env: - COSIGN_EXPERIMENTAL: "true" - # This step uses the identity token to provision an ephemeral certificate - # against the sigstore community Fulcio instance. - run: echo "${{ steps.meta.outputs.tags }}" | xargs -I {} cosign sign {}@${{ steps.build-and-push.outputs.digest }} diff --git a/.gitignore b/.gitignore index f80d64b..2435e48 100644 --- a/.gitignore +++ b/.gitignore @@ -1,37 +1,44 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules/ -/.pnp -.pnp.js - -# testing -/coverage - -# next.js -/.next/ -/out/ - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# local env files -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo - -.env +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules/ +/.pnp +.pnp.js + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +.pnpm-debug.log* + +# local env files +.env* +!.env.example + +# React Email preview and exported templates +/.react-email/ +/email-out/ + +# vercel +.vercel + +# typescript +*.tsbuildinfo +/next-env.d.ts + +/.yarn +/.yarnrc.yml diff --git a/frontend/.prettierignore b/.prettierignore similarity index 100% rename from frontend/.prettierignore rename to .prettierignore diff --git a/frontend/.prettierrc b/.prettierrc similarity index 100% rename from frontend/.prettierrc rename to .prettierrc diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..af3b7c8 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ + + +## This is NOT the Next.js you know + +This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices. + +This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean. + + diff --git a/README.md b/README.md index adb44c6..cf5a6da 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,96 @@ -# Official Website - -## About - -This is the official website for the Manipal OSF organisation. - -## Contributing - -A prerequisite knowledge of [React](https://beta.reactjs.org/), [Next.js](https://nextjs.org/docs/getting-started), [TailwindCSS](https://tailwindcss.com/docs/installation) and basic HTML is required for the frontend and [PayloadCMS](https://payloadcms.com/docs/getting-started/what-is-payload) for the backend along with [docker](https://docs.docker.com/get-started/), [GitHub Actions](https://docs.github.com/en/actions/learn-github-actions) for CI/CD. - -Although if you are not familiar with any/all of these, you can still contibute in a particular domain or by finding bugs in the site. - -## Additional Information - -- If you are a sysadmin, check out [the configuration document](./backend/SERVER_CONFIG.md). +# Manipal OSF website + +The official Manipal OSF website is a single Next.js application at the repository root. It uses TypeScript, React, Tailwind CSS, and the App Router. Blog posts live in the repository as Markdown files. + +## Development + +Use Node.js 24 LTS (minimum 22) and PNPM 11.2.2. Run commands from the repository root: + +```sh +pnpm install +pnpm dev +``` + +Open http://localhost:3000. No environment variables or separate service are required for the existing site. + +Development and production builds use Turbopack. + +```sh +pnpm typecheck +pnpm lint +pnpm format +``` + +CI runs the production build with `pnpm build`. A deployed Node.js application starts with `pnpm start`. Configure hosting to use the repository root, with `pnpm install --frozen-lockfile` as the install command and `pnpm build` as the build command. + +## Blog posts + +See [website configuration](./WEBSITE_CONFIG.md) for the Markdown format, images, and publishing instructions. The blog initially displays “No posts yet.” Add your own content to `content/posts/`; existing hosted content is not copied automatically. + +The `@next/mdx` component registry lives in `mdx-components.tsx` at the repository root, beside `app/`, as required by Next.js. Keep it there when adding MDX components. The filesystem blog continues to render Markdown using `MarkdownRenderer`. + +## UI components + +shadcn/ui is configured with the Base UI Nova style, TypeScript, React Server Components, Lucide icons, and the `@/*` import alias pointing to the repository root. Its CSS variables extend the existing Nord light and Catppuccin dark themes, using the existing `data-theme` switch. + +Import the initial Button from `@/components/ui/button` and shared class helpers from `@/lib/utils`. Add more components from the repository root: + +```sh +pnpm exec shadcn add input +``` + +`components.json` points to `styles/globals.css`; Tailwind v4 does not need a separate Tailwind configuration file. Keep the existing palettes and `data-theme` convention when adding theme tokens. + +## Environment variables + +The public site runs without service credentials. When preparing backend features, copy the example file: + +```sh +cp .env.example .env.local +``` + +`lib/env.ts` uses T3 Env and Zod to validate settings during development and builds. Service settings are optional, and blank values are treated as unset. Values supplied must be valid. Import `env` from `@/lib/env` in server code instead of reading `process.env` directly. All settings below are server variables; do not expose credentials using `NEXT_PUBLIC_`. + +| Variable | Purpose | +| -------------------- | -------------------------------------------------------------------------------------- | +| `DATABASE_URL` | Neon PostgreSQL connection string (`postgres://` or `postgresql://`). | +| `BETTER_AUTH_SECRET` | Future auth secret of at least 32 characters; generate with `openssl rand -base64 32`. | +| `BETTER_AUTH_URL` | Future auth server origin, such as `http://localhost:3000`. | +| `RESEND_API_KEY` | API key required when calling `getResend()`. | +| `RESEND_FROM_EMAIL` | Plain sender email address on a Resend-verified domain, for future sending features. | + +Real `.env*` files are ignored by Git; `.env.example` contains no credentials and is tracked. + +## Database and authentication + +Drizzle ORM, Drizzle Kit, the Neon serverless driver, Better Auth, and its Drizzle adapter are installed for later use. Database clients, application/auth schemas, migrations, auth routes, and login pages are deferred. The blog still uses repository Markdown files. + +When implementing these features, configure a Neon PostgreSQL database with the validated `DATABASE_URL`, use `drizzle-orm/neon-http` for HTTP queries (or `drizzle-orm/neon-serverless` when interactive transactions are needed), and connect Better Auth through `@better-auth/drizzle-adapter` with PostgreSQL provider `pg`. Generate the auth schema from the final auth configuration before creating Drizzle migrations. Add the auth Route Handler under `app/api/auth/[...all]/route.ts` and keep client auth code separate from server credentials. + +Drizzle Kit runs outside the Next.js runtime. Before adding its configuration, use `@next/env` to load the root `.env*` files so CLI commands follow the same environment loading rules as Next.js. + +## Email templates + +Resend and React Email are ready for future email features. Components and rendering utilities use the current `react-email` package; `@react-email/ui` provides local previews. + +```sh +pnpm email:dev +``` + +Open http://localhost:3001 to preview `emails/example.tsx`. Previews need no API key and send no email. + +Server code can import `getResend` from `@/lib/resend` to obtain a lazily initialized Resend client. It throws a clear error if `RESEND_API_KEY` is unset when called. When adding a sending feature, pass a verified sender, recipient, subject, and React Email template to `getResend().emails.send(...)`, and handle the returned `error`. There are no sending endpoints or automatic email triggers yet. + +## Server features + +Add server endpoints as Route Handlers in `app/**/route.ts`, using the Web `Request` and `Response` APIs or Next.js `NextRequest` and `NextResponse`. Keep private environment variables server-side and put reusable server logic in `lib/`. Async Server Components, post metadata, and `generateStaticParams` call the server-only filesystem blog loader directly; it does not make HTTP calls. + +Use a deployment that supports the Next.js Node.js runtime when adding Route Handlers or request-time server rendering. + +## Page transitions + +Internal site links use `components/TransitionLink` and `next-transition-router` to finish the current page's exit animation before navigating, then reset scroll and animate the new page in. The shared header and footer stay mounted. Browser Back/Forward uses native navigation and scroll restoration without page transitions. Use the transition-aware Link with `scroll={false}` when adding site navigation; external links and Markdown anchors retain normal browser behavior. + +## Contributing + +React, Next.js, TypeScript, Tailwind CSS, and basic HTML knowledge are useful. Contributions to content and bug reports are welcome too. diff --git a/WEBSITE_CONFIG.md b/WEBSITE_CONFIG.md new file mode 100644 index 0000000..ca0f61c --- /dev/null +++ b/WEBSITE_CONFIG.md @@ -0,0 +1,38 @@ +# Website configuration + +All source, public assets, and configuration are at the repository root. Run `pnpm dev` from this directory. No environment variables are needed for the existing site. Future server features can read private values from `.env.local`; never prefix secrets with `NEXT_PUBLIC_`. + +## Write a blog post + +Create `content/posts/your-post.md`: + +```md +--- +id: your-post +title: Your post title +authors: + - Your Name +publishDate: '2026-10-08' +status: draft +coverImage: /images/your-post.jpg +--- + +## Introduction + +Write standard Markdown here, including **bold text**, links, fenced code blocks, and GitHub-flavored Markdown tables and task lists. +``` + +- `id` determines the URL (`/blog/your-post`), independently of the filename. IDs must be unique across drafts and published posts and contain only letters, numbers, underscores, or hyphens. Numeric IDs are supported for preserving existing URLs. +- `title` is a non-empty string; `authors` is a non-empty array of names. +- `publishDate` is a quoted, valid `YYYY-MM-DD` date. Published posts sort newest first. This date is display metadata, not a publishing schedule. +- `status` must be `draft` or `published`. Only published posts appear in the list or have public detail pages. There is no draft preview. +- `coverImage` is optional. Place the example image in `public/images/your-post.jpg`; omit this field to display the existing placeholder. Images must use local public paths beginning with a single `/`. +- The body supports Markdown rather than JSX or raw HTML. Start headings at `##`, since the page already renders the title as an `h1`. + +The directory initially contains only `.gitkeep`, so the blog shows “No posts yet.” Invalid metadata and duplicate IDs produce errors identifying the offending post or ID. + +## Publish + +Change `status` to `published`, commit the Markdown and any images, and deploy. App Router Server Components and `generateStaticParams` generate blog pages during the production build; additions, edits, and removals require a rebuild and deployment. During development, the content loader reads files on each page request. Reload the page to see content changes; new published IDs work without restarting the dev server. + +Unknown or draft post URLs return 404. Historical hosted posts must be exported and converted to this format separately, preserving their IDs if their existing URLs need to keep working. diff --git a/app/blog/[uid]/page.tsx b/app/blog/[uid]/page.tsx new file mode 100644 index 0000000..4dccfea --- /dev/null +++ b/app/blog/[uid]/page.tsx @@ -0,0 +1,65 @@ +import type { Metadata } from 'next'; +import { notFound } from 'next/navigation'; +import Link from '../../../components/TransitionLink'; +import { ArrowLeft } from 'lucide-react'; +import { getPost, getPublishedPosts } from '../../../lib/posts'; +import CustomImage from '../../../components/CustomImage'; +import MarkdownRenderer from '../../../components/MarkdownRenderer'; + +export const dynamicParams = false; + +export async function generateStaticParams() { + return (await getPublishedPosts()).map((post) => ({ uid: post.id })); +} + +type PostPageProps = { params: Promise<{ uid: string }> }; + +async function loadPost(params: PostPageProps['params']) { + const { uid } = await params; + const post = await getPost(uid); + if (!post) notFound(); + return post; +} + +export async function generateMetadata({ + params, +}: PostPageProps): Promise { + const post = await loadPost(params); + return { title: `${post.title} | Manipal OSF` }; +} + +const IndividualBlogPage = async ({ params }: PostPageProps) => { + const data = await loadPost(params); + return ( + <> +
+

{data.title}

+ +
+
+

WRITTEN BY

+

{data.authors.join(', ')}

+
+
+

PUBLISHED ON

+ +
+
+
+ + + + Return to blog + +
+ + ); +}; + +export default IndividualBlogPage; diff --git a/app/blog/page.tsx b/app/blog/page.tsx new file mode 100644 index 0000000..800ea53 --- /dev/null +++ b/app/blog/page.tsx @@ -0,0 +1,27 @@ +import type { Metadata } from 'next'; +import BlogCard from '../../components/BlogCard'; +import { getPublishedPosts } from '../../lib/posts'; + +export const metadata: Metadata = { title: 'Blog | Manipal OSF' }; + +const BlogPage = async () => { + const posts = await getPublishedPosts(); + return ( + <> +
+

Featured

+ {posts.length === 0 ? ( +

No posts yet.

+ ) : ( +
+ {posts.map((item) => ( + + ))} +
+ )} +
+ + ); +}; + +export default BlogPage; diff --git a/app/error.tsx b/app/error.tsx new file mode 100644 index 0000000..88c4bf1 --- /dev/null +++ b/app/error.tsx @@ -0,0 +1,7 @@ +'use client'; + +import ServerError from '../components/ServerError'; + +export default function ErrorPage({ retry }: { retry: () => void }) { + return ; +} diff --git a/app/events/page.tsx b/app/events/page.tsx new file mode 100644 index 0000000..bfcea77 --- /dev/null +++ b/app/events/page.tsx @@ -0,0 +1,10 @@ +import type { Metadata } from 'next'; +import UnderDev from '../../components/UnderDev'; + +export const metadata: Metadata = { title: 'Under development' }; + +const Events = () => { + return ; +}; + +export default Events; diff --git a/app/global-error.tsx b/app/global-error.tsx new file mode 100644 index 0000000..14212e1 --- /dev/null +++ b/app/global-error.tsx @@ -0,0 +1,18 @@ +'use client'; + +import ServerError from '../components/ServerError'; +import { themeScript } from '../lib/theme-script'; +import '../styles/globals.css'; + +export default function GlobalError({ retry }: { retry: () => void }) { + return ( + + +