# CLAUDE.md — DTNews

> This file is read by Claude Code automatically on every session.
> It defines project structure, conventions, and rules for AI-assisted development.

---

## 🏢 Project Info

- **Company:** DivineTechs IT Company, Viramgam, Gujarat, India
- **Product:** DTNews — Indian News Web Application (web + Flutter mobile companion)
- **Type:** Next.js Web Application
- **Backend:** Laravel (raw SQL, no migrations, MySQL)
- **Frontend:** Next.js 15 + TypeScript + Tailwind CSS + Redux Toolkit + next-intl
- **API Base URL:** `https://dtnews.divinetechs.com/public/api/`
- **Auth Type:** No Token — all APIs are public (no Authorization header)

---

## 🛠️ Tech Stack

| Layer            | Technology               |
| ---------------- | ------------------------ |
| Framework        | Next.js 15 (App Router)  |
| Language         | TypeScript (strict mode) |
| Styling          | Tailwind CSS             |
| State Management | Redux Toolkit            |
| HTTP             | Axios                    |
| i18n             | next-intl                |
| Forms            | React Hook Form + Zod    |
| Icons            | Lucide React             |
| Package Manager  | npm                      |

---

## 📁 Project Structure

```
dtnews/
├── public/
│   ├── icons/
│   ├── images/
│   ├── fonts/
│   └── locales/
│       ├── en/
│       │   ├── common.json
│       │   ├── home.json
│       │   ├── article.json
│       │   ├── category.json
│       │   ├── topics.json
│       │   ├── feeds.json
│       │   ├── shorts.json
│       │   ├── search.json
│       │   ├── epaper.json
│       │   └── contact.json
│       ├── gu/
│       │   └── [same files as en/]
│       ├── hi/
│       │   └── [same files as en/]
│       └── ta/
│           └── [same files as en/]
│
├── src/
│   ├── app/
│   │   ├── [locale]/
│   │   │   ├── (main)/
│   │   │   │   ├── page.tsx                  # Home
│   │   │   │   ├── article/
│   │   │   │   │   └── [slug]/
│   │   │   │   ├── category/
│   │   │   │   │   └── [slug]/
│   │   │   │   ├── topics/
│   │   │   │   ├── feeds/
│   │   │   │   ├── shorts/
│   │   │   │   ├── search/
│   │   │   │   ├── epaper/
│   │   │   │   └── contact/
│   │   │   ├── (auth)/
│   │   │   │   ├── login/
│   │   │   │   └── signup/
│   │   │   └── layout.tsx
│   │   ├── api/
│   │   ├── layout.tsx
│   │   └── not-found.tsx
│   │
│   ├── features/
│   │   ├── home/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   ├── services/
│   │   │   └── types/
│   │   ├── article/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   ├── services/
│   │   │   └── types/
│   │   ├── category/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   ├── services/
│   │   │   └── types/
│   │   ├── topics/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   ├── services/
│   │   │   └── types/
│   │   ├── feeds/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   ├── services/
│   │   │   └── types/
│   │   ├── shorts/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   ├── services/
│   │   │   └── types/
│   │   ├── search/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   ├── services/
│   │   │   └── types/
│   │   ├── epaper/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   ├── services/
│   │   │   └── types/
│   │   ├── live-tv/
│   │   │   ├── components/
│   │   │   ├── hooks/
│   │   │   ├── services/
│   │   │   └── types/
│   │   └── newsletter/
│   │       ├── components/
│   │       ├── hooks/
│   │       ├── services/
│   │       └── types/
│   │
│   ├── components/
│   │   ├── ui/
│   │   │   ├── Button.tsx
│   │   │   ├── Input.tsx
│   │   │   ├── Badge.tsx
│   │   │   ├── Card.tsx
│   │   │   ├── Modal.tsx
│   │   │   ├── LiveDot.tsx
│   │   │   ├── BreakingPill.tsx
│   │   │   └── LocaleSwitcher.tsx
│   │   └── layout/
│   │       ├── Header.tsx
│   │       ├── Footer.tsx
│   │       ├── BreakingTicker.tsx
│   │       └── PageWrapper.tsx
│   │
│   ├── hooks/
│   │   └── useLocale.ts
│   │
│   ├── i18n/
│   │   ├── config.ts
│   │   ├── request.ts
│   │   ├── routing.ts
│   │   └── index.ts
│   │
│   ├── lib/
│   │   ├── utils/
│   │   │   ├── formatDate.ts
│   │   │   ├── formatViews.ts
│   │   │   ├── formatReadTime.ts
│   │   │   └── slugify.ts
│   │   ├── constants/
│   │   │   ├── routes.ts
│   │   │   ├── keys.ts
│   │   │   └── locales.ts
│   │   ├── validators/
│   │   │   ├── contact.validator.ts
│   │   │   └── newsletter.validator.ts
│   │   └── formatters/
│   │       └── locale.formatter.ts
│   │
│   ├── services/
│   │   └── api.client.ts
│   │
│   ├── store/
│   │   ├── slices/
│   │   │   ├── homeSlice.ts
│   │   │   ├── articleSlice.ts
│   │   │   ├── categorySlice.ts
│   │   │   ├── topicsSlice.ts
│   │   │   ├── feedsSlice.ts
│   │   │   ├── shortsSlice.ts
│   │   │   ├── searchSlice.ts
│   │   │   ├── epaperSlice.ts
│   │   │   └── liveTvSlice.ts
│   │   ├── hooks.ts
│   │   └── store.ts
│   │
│   ├── middleware/
│   │   ├── locale.middleware.ts
│   │   └── index.ts
│   │
│   ├── branding/
│   │   ├── colors.ts
│   │   ├── fonts.ts
│   │   ├── images.ts
│   │   ├── text.ts
│   │   └── index.ts
│   │
│   └── types/
│       ├── api/
│       │   ├── article.types.ts
│       │   ├── channel.types.ts
│       │   └── topic.types.ts
│       └── ui/
│
├── .env.local
├── .env.example
├── middleware.ts
├── next.config.ts
├── tailwind.config.ts
├── tsconfig.json
├── eslint.config.mjs
├── package.json
└── README.md
```

---

## 🎨 Branding — DTNews

```ts
// src/branding/colors.ts
export const colors = {
  brand: "#C1272D", // DTNews Primary Red — CTAs, active states, breaking pills
  brandHover: "#ED2027", // Brighter accent red — LIVE indicators, hover
  brandLight: "#FEE2E2", // Red-tinted chips / hover backgrounds
  ink: "#0E0E11", // Primary text, dark surfaces
  textSecondary: "#6B7280",
  textTertiary: "#9CA3AF", // Mono timestamps
  background: "#F8F9FB", // Page background
  surface: "#FFFFFF", // Cards
  cream: "#FAF7F2", // Warm panels, e-paper stage
  border: "#E5E7EB",
  success: "#047857", // Markets up
  error: "#C1272D", // Markets down (reuses brand)
  live: "#ED2027", // Pulsing dot, on-air indicators
};
```

```ts
// tailwind.config.ts — extend with branding tokens
theme: {
  extend: {
    colors: {
      brand: '#C1272D',
      'brand-hover': '#ED2027',
      'brand-light': '#FEE2E2',
      ink: '#0E0E11',
      surface: '#FFFFFF',
      cream: '#FAF7F2',
      'text-secondary': '#6B7280',
      'text-tertiary': '#9CA3AF',
      border: '#E5E7EB',
      live: '#ED2027',
    },
    fontFamily: {
      sans: ['var(--font-geist)', 'system-ui', 'sans-serif'],
      mono: ['var(--font-geist-mono)', 'ui-monospace', 'monospace'],
      italic: ['var(--font-instrument-serif)', 'Georgia', 'serif'],
      serif: ['var(--font-source-serif)', 'Georgia', 'serif'],
    },
  }
}
```

**Rules:**

- Use `text-brand`, `bg-brand`, `border-brand` — never raw `#C1272D`
- Cards use `bg-surface` with `border border-border`
- Page background is always `bg-background`
- **Italic Instrument Serif** carries editorial voice — DT wordmark, section headlines, pull quotes
- **Geist Mono** carries metadata — timestamps, view counts, page numbers, stats
- **Source Serif 4** carries long-form body and editorial card titles
- **Geist** carries UI labels, buttons, body chrome

---

## 🔌 API Integration — No Token

> ⚠️ DTNews uses NO authentication token. All APIs are public.
> Never add Authorization headers. Never read from localStorage for tokens.

```ts
// src/services/api.client.ts
import axios from "axios";

const apiClient = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: {
    "Content-Type": "application/json",
    Accept: "application/json",
  },
  // ❌ NO auth interceptor — this app has no token auth
});

// Response interceptor for error handling only
apiClient.interceptors.response.use(
  (response) => response,
  (error) => {
    console.error("API Error:", error.response?.data || error.message);
    return Promise.reject(error);
  },
);

export default apiClient;
```

```ts
// Feature service pattern
// features/article/services/article.service.ts
import apiClient from "@/services/api.client";
import type { Article, ArticlesResponse } from "../types/article.types";

export const articleService = {
  getAll: (params?: Record<string, string>) =>
    apiClient.get<ArticlesResponse>("/articles", { params }),

  getBySlug: (slug: string) => apiClient.get<Article>(`/articles/${slug}`),

  getByCategory: (slug: string, params?: Record<string, string>) =>
    apiClient.get<ArticlesResponse>(`/category/${slug}`, { params }),

  getBreaking: () =>
    apiClient.get<Article[]>("/breaking", {
      // Live ticker — never cache
      headers: { "Cache-Control": "no-cache" },
    }),

  getTrending: () => apiClient.get<Article[]>("/trending"),

  search: (query: string, filters?: Record<string, string>) =>
    apiClient.get<ArticlesResponse>("/search", {
      params: { q: query, ...filters },
    }),
};
```

---

## 🗃️ Redux Toolkit Pattern

> State Management: Redux Toolkit only (no RTK Query for this project)

### Slice Pattern

```ts
// store/slices/articleSlice.ts
import { createSlice, createAsyncThunk, PayloadAction } from "@reduxjs/toolkit";
import { articleService } from "@/features/article/services/article.service";
import type { Article } from "@/features/article/types/article.types";

// Async thunk
export const fetchArticles = createAsyncThunk(
  "articles/fetchAll",
  async (params: Record<string, string> | undefined, { rejectWithValue }) => {
    try {
      const response = await articleService.getAll(params);
      return response.data;
    } catch (error: unknown) {
      return rejectWithValue("Failed to fetch articles");
    }
  },
);

interface ArticleState {
  items: Article[];
  isLoading: boolean;
  error: string | null;
  currentPage: number;
  totalPages: number;
}

const initialState: ArticleState = {
  items: [],
  isLoading: false,
  error: null,
  currentPage: 1,
  totalPages: 1,
};

export const articleSlice = createSlice({
  name: "articles",
  initialState,
  reducers: {
    resetArticles: (state) => {
      state.items = [];
      state.currentPage = 1;
    },
  },
  extraReducers: (builder) => {
    builder
      .addCase(fetchArticles.pending, (state) => {
        state.isLoading = true;
        state.error = null;
      })
      .addCase(fetchArticles.fulfilled, (state, action) => {
        state.isLoading = false;
        state.items = action.payload.data;
        state.totalPages = action.payload.last_page;
      })
      .addCase(fetchArticles.rejected, (state, action) => {
        state.isLoading = false;
        state.error = action.payload as string;
      });
  },
});

export const { resetArticles } = articleSlice.actions;
export default articleSlice.reducer;
```

### Root Store

```ts
// store/store.ts — only imports, never defines slices here
import { configureStore } from "@reduxjs/toolkit";
import homeReducer from "@store/slices/homeSlice";
import articleReducer from "@store/slices/articleSlice";
import categoryReducer from "@store/slices/categorySlice";
import topicsReducer from "@store/slices/topicsSlice";
import feedsReducer from "@store/slices/feedsSlice";
import shortsReducer from "@store/slices/shortsSlice";
import searchReducer from "@store/slices/searchSlice";
import epaperReducer from "@store/slices/epaperSlice";
import liveTvReducer from "@store/slices/liveTvSlice";

export const store = configureStore({
  reducer: {
    home: homeReducer,
    articles: articleReducer,
    category: categoryReducer,
    topics: topicsReducer,
    feeds: feedsReducer,
    shorts: shortsReducer,
    search: searchReducer,
    epaper: epaperReducer,
    liveTv: liveTvReducer,
  },
});

export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;
```

### Typed Hooks

```ts
// store/hooks.ts
import { useDispatch, useSelector } from "react-redux";
import type { RootState, AppDispatch } from "./store";

export const useAppDispatch = () => useDispatch<AppDispatch>();
export const useAppSelector = <T>(selector: (state: RootState) => T) =>
  useSelector(selector);
```

---

## 🌐 i18n — DTNews

```ts
// src/i18n/config.ts
export const locales = ["en", "hi", "gu", "ar"] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = "en";
export const rtlLocales: Locale[] = [];
```

```ts
// src/lib/constants/locales.ts
export const SUPPORTED_LOCALES = ["en", "hi", "gu", "ar"] as const;
export const DEFAULT_LOCALE = "en";
export const RTL_LOCALES: string[] = [];

export const LOCALE_LABELS = {
  en: "English",
  hi: "हिन्दी",
  gu: "ગુજરાતી",
  ar: "العربية",
};
```

**Translation file structure:**

```json
// public/locales/en/common.json
{
  "nav": { "home": "Home", "politics": "Politics", "world": "World", "tech": "Tech", "sports": "Sports", "business": "Business", "topics": "Topics", "feeds": "Feeds", "shorts": "Shorts", "epaper": "E-Paper" },
  "actions": { "read": "Read", "watch": "Watch", "listen": "Listen", "share": "Share", "save": "Save", "follow": "Follow", "following": "Following" },
  "labels": { "breaking": "Breaking", "live": "Live", "trending": "Trending", "exclusive": "Exclusive", "premium": "Premium" },
  "meta": { "min_read": "min read", "views": "views", "watching": "watching", "subscribers": "subscribers", "updated": "Updated" }
}

// public/locales/en/article.json
{
  "by": "By",
  "comments": "Comments",
  "related": "Related Articles",
  "most_read": "Most Read",
  "post_comment": "Post Comment",
  "share_thoughts": "Share your thoughts on this story…"
}
```

**Locale Rule:**

```tsx
// src/app/[locale]/layout.tsx
const dir = rtlLocales.includes(locale) ? 'rtl' : 'ltr'
<html lang={locale} dir={dir}>
```

---

## 🗺️ Routes — DTNews

```ts
// src/lib/constants/routes.ts
export const ROUTES = {
  HOME: "/",
  ARTICLE: (slug: string) => `/article/${slug}`,
  TOPICS: "/topics",
  FEEDS: "/feeds",
  SHORTS: "/shorts",
  SEARCH: "/search",
  EPAPER: "/epaper",
  LOGIN: "/login",
  SIGNUP: "/signup",
  CONTACT: "/contact",
} as const;
```

---

## 🔐 Middleware — No Auth

> ⚠️ No auth middleware needed — DTNews has no protected routes.
> Only locale middleware is active.

```ts
// src/middleware/index.ts
import { localeMiddleware } from "./locale.middleware";
import type { NextRequest } from "next/server";

export default function middleware(request: NextRequest) {
  return localeMiddleware(request);
  // No auth middleware — all routes are public
}

export { config } from "./locale.middleware";
```

---

## 🔑 Core Conventions

### File Naming

- Components: `PascalCase.tsx` → `ArticleCard.tsx`
- Hooks: `camelCase.ts` with `use` prefix → `useArticles.ts`
- Services: `camelCase.service.ts` → `article.service.ts`
- Store slices: `camelCase.ts` → `articleSlice.ts`
- Types: `camelCase.types.ts` → `article.types.ts`
- Utils: `camelCase.ts` → `formatDate.ts`
- Constants: `UPPER_SNAKE_CASE` values, `camelCase.ts` files

### Import Order

```ts
// 1. React / Next.js
import { useState } from "react";
import { useRouter } from "next/navigation";

// 2. Third-party
import { useTranslations } from "next-intl";
import axios from "axios";

// 3. Store / hooks
import { useAppSelector, useAppDispatch } from "@/store/hooks";

// 4. Features
import { fetchArticles } from "@/store/slices/articleSlice";

// 5. Shared components
import { Button } from "@/components/ui/Button";

// 6. Lib / utils
import { formatDate } from "@/lib/utils/formatDate";

// 7. Types
import type { Article } from "./types/article.types";
```

---

## 🚫 Hard Rules — Never Violate

1. **Never hardcode colors** — always use Tailwind tokens from `branding/colors.ts`
2. **Never add Authorization header** — DTNews has no token auth
3. **Never create axios instance outside** `services/api.client.ts`
4. **Never define Redux slice inside** `features/[name]/store/`
5. **Never use RTK Query** — use `createAsyncThunk` + Redux Toolkit only
6. **Never put feature-specific code** in `components/` or `hooks/` root
7. **Never use `any` type** — always define proper TypeScript interfaces
8. **Never use Laravel migrations** — raw SQL only on backend
9. **Never commit `.env.local`** — only `.env.example` is committed
10. **Never use inline styles** — Tailwind classes only
11. **Never skip translation keys** — all user-visible text via `next-intl`
12. **Never use localStorage for auth** — no auth in this project
13. **Never use emoji** unless already in the prototype — DTNews reads as serious editorial
14. **Never render metadata in Geist sans** — timestamps, view counts, stats use Geist Mono
15. **Never invent new brand reds** — only `#C1272D` (brand) and `#ED2027` (accent/live) for red

---

## 🧪 Code Quality Checklist

Before completing any task, verify:

- [ ] TypeScript: no `any` types used
- [ ] No Authorization header added anywhere
- [ ] i18n: all visible text uses translation keys
- [ ] Branding: no hardcoded `#C1272D` or other colors
- [ ] Imports: correct import order followed
- [ ] File location: correct folder per feature decision guide
- [ ] Naming: follows naming conventions
- [ ] Translation keys: added to ALL 4 locale files (en, hi, gu, ta)
- [ ] No unused imports or variables
- [ ] Component has TypeScript interface for props
- [ ] Redux: using `createAsyncThunk` not RTK Query
- [ ] Italic serif for editorial voice, mono for metadata
- [ ] Live state uses red pulsing dot — never green / orange

---

## 📝 When Adding a New Feature

Follow this exact order:

1. Create `src/features/[name]/` with subfolders: `components/`, `hooks/`, `services/`, `types/`
2. Define types → `features/[name]/types/[name].types.ts`
3. Create service → `features/[name]/services/[name].service.ts`
4. Create Redux slice → `store/slices/[name]Slice.ts`
5. Register reducer in `store/store.ts`
6. Create hooks → `features/[name]/hooks/use[Name].ts`
7. Build components → `features/[name]/components/`
8. Add page → `src/app/[locale]/(main)/[name]/page.tsx`
9. Add route → `lib/constants/routes.ts`
10. Add translation keys to ALL files under `public/locales/en|hi|gu|ta/`

---

## 📦 Common Commands

```bash
# Development
npm run dev

# Build
npm run build

# Type check
npx tsc --noEmit

# Lint
npm run lint

# Install package
npm install [package-name]
```

---

## 🌍 Environment Variables

```bash
# .env.example
NEXT_PUBLIC_API_URL=https://dtnews.divinetechs.com/public/api/
NEXT_PUBLIC_APP_NAME=DTNews
NEXT_PUBLIC_DEFAULT_LOCALE=en
```

---

## ⚠️ DTNews Specific Notes

- **No auth/login system** — Login/Signup pages exist as design-only stubs; all news pages and APIs are publicly accessible
- **No protected routes** — middleware only handles locale, not auth
- **No token storage** — never read/write auth tokens to localStorage or cookies
- **Live state is red** — pulsing dot, red pill with white animated dot, ring-pulse on circular avatars. Never green / orange.
- **Italic Instrument Serif** carries the DT wordmark and section headlines — never use it for UI labels
- **Geist Mono** is the metadata font — timestamps, view counts, page numbers, ranks, stats
- **Source Serif 4** for long-form article body and editorial card titles
- **Breaking ticker** — marquee with `revalidate: 0` or streaming, every minute
- **Live TV / Shorts** — viewer counts and channel cards use SSE / WebSocket where possible, fall back to short polling
- **E-Paper** — PDF served from CDN, page thumbs are pre-rendered; `next/image` with `unoptimized` on PDF previews
- **Sticky 3-column shells** — left rail · content · right rail. Rails use `position: sticky; top: 96px` (offset for the 72px frosted navbar).
- **`.wrap` container** caps at 1280–1360px with 40px gutters. Single-column collapse at ≤1000–1100px.
- **Real imagery only** — Unsplash photos in mockups, never AI-illustrated SVG portraits
- **Date / number formatting** — use `lib/utils/formatDate.ts` and `lib/utils/formatViews.ts` respecting locale (IST in mono font)
- **Slugified URLs** — articles and categories use slug, never numeric IDs

---

_Maintained by DivineTechs IT Company — Viramgam, Gujarat_
_Instagram: @aiwithck_
