basic structure setup
This commit is contained in:
commit
71dfd86187
89 files changed
+41490
No files matched your search
+92
@@ -0,0 +1,92 @@
|
|||||||
|
# Dependencies
|
||||||
|
node_modules/
|
||||||
|
.pnp/
|
||||||
|
.pnp.js
|
||||||
|
|
||||||
|
# Build outputs
|
||||||
|
dist/
|
||||||
|
dist-ssr/
|
||||||
|
build/
|
||||||
|
*.local
|
||||||
|
next-env.d.ts
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Database
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite-journal
|
||||||
|
*.db
|
||||||
|
*.db-shm
|
||||||
|
*.db-wal
|
||||||
|
database.sqlite
|
||||||
|
data/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
server.log
|
||||||
|
error.log
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
coverage/
|
||||||
|
.nyc_output/
|
||||||
|
*.lcov
|
||||||
|
.vscode-test/
|
||||||
|
|
||||||
|
# Cache
|
||||||
|
.cache/
|
||||||
|
.turbo/
|
||||||
|
.vite/
|
||||||
|
.tsbuildinfo
|
||||||
|
.eslintcache
|
||||||
|
.stylelintcache
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
|
|
||||||
|
# OS
|
||||||
|
Thumbs.db
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
|
||||||
|
# Strapi specific
|
||||||
|
cms/cms/.tmp/
|
||||||
|
cms/cms/.cache/
|
||||||
|
cms/cms/node_modules/
|
||||||
|
cms/cms/.env
|
||||||
|
cms/cms/dist/
|
||||||
|
cms/cms/public/uploads/
|
||||||
|
cms/cms/admin/
|
||||||
|
|
||||||
|
# Frontend specific
|
||||||
|
frontend/node_modules/
|
||||||
|
frontend/dist/
|
||||||
|
frontend/.next/
|
||||||
|
frontend/.turbo/
|
||||||
|
frontend/.cache/
|
||||||
|
|
||||||
|
# Backend specific
|
||||||
|
backend/node_modules/
|
||||||
|
backend/dist/
|
||||||
|
backend/database.sqlite
|
||||||
|
backend/data/
|
||||||
|
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# Agent Guidelines for placebo.mk
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
News site in Macedonia with sarcastic tone. Minimalistic design using TanStack stack.
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
- **Frontend**: TanStack (React 19, Query, Router) + Vite + Tailwind CSS + shadcn/ui
|
||||||
|
- **Backend**: NestJS + TypeORM + SQLite
|
||||||
|
- **CMS**: Strapi (in /cms)
|
||||||
|
|
||||||
|
## Build Commands
|
||||||
|
|
||||||
|
### Backend (NestJS)
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
npm run start:dev # Watch mode
|
||||||
|
npm run build
|
||||||
|
npm run start:prod
|
||||||
|
npm run lint # Lint
|
||||||
|
npm run lint:fix # Auto-fix
|
||||||
|
npm run type-check # TypeScript check
|
||||||
|
npm test # All tests
|
||||||
|
npm test app.service.spec.ts # Single file
|
||||||
|
npm test -t "should" # By test name
|
||||||
|
npm run test:cov # Coverage
|
||||||
|
npm run test:e2e # E2E tests
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (TanStack)
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev # Vite dev server
|
||||||
|
npm run build
|
||||||
|
npm run preview
|
||||||
|
npm run lint
|
||||||
|
npm run lint:fix
|
||||||
|
npm run type-check
|
||||||
|
npm test # All tests
|
||||||
|
npm test Header.test.tsx # Single file
|
||||||
|
npm test -t "renders" # By name
|
||||||
|
npm run test:ui # Vitest UI
|
||||||
|
```
|
||||||
|
|
||||||
|
## Code Style
|
||||||
|
|
||||||
|
### Formatting & TypeScript
|
||||||
|
- Use Prettier (2 spaces, single quotes, trailing commas, 100 char max)
|
||||||
|
- Strict TypeScript - no implicit `any`, avoid `any`, use `unknown`
|
||||||
|
- Explicit return types for public methods
|
||||||
|
- Prefer interfaces over types for objects
|
||||||
|
- Use `readonly` for immutable properties
|
||||||
|
|
||||||
|
### Naming Conventions
|
||||||
|
- **Files**: kebab-case (`user-profile.ts`, `auth.service.ts`)
|
||||||
|
- **Classes**: PascalCase (`UserService`, `AuthGuard`)
|
||||||
|
- **Functions/Variables**: camelCase (`getUser`, `isLoading`)
|
||||||
|
- **Constants**: UPPER_SNAKE_CASE (`MAX_RETRIES`)
|
||||||
|
- **Private members**: underscore prefix (`_cache`, `_validate()`)
|
||||||
|
- **Interfaces**: PascalCase, no 'I' prefix (`User`, `ApiResponse`)
|
||||||
|
|
||||||
|
### Import Order
|
||||||
|
- External libraries first, then internal modules, then relative imports
|
||||||
|
|
||||||
|
|
||||||
|
### File Structure
|
||||||
|
```
|
||||||
|
backend/src/
|
||||||
|
modules/feature-name/
|
||||||
|
feature-name.module.ts
|
||||||
|
feature-name.controller.ts
|
||||||
|
feature-name.service.ts
|
||||||
|
feature-name.entity.ts
|
||||||
|
feature-name.dto.ts
|
||||||
|
common/{decorators,filters,guards,interceptors,pipes}
|
||||||
|
config/
|
||||||
|
|
||||||
|
frontend/src/
|
||||||
|
components/{ui,layout,features}/
|
||||||
|
hooks/
|
||||||
|
lib/
|
||||||
|
queries/
|
||||||
|
routes/
|
||||||
|
types/
|
||||||
|
utils/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling
|
||||||
|
- **Backend**: Use NestJS exceptions (`NotFoundException`, `BadRequestException`), custom exceptions, Logger
|
||||||
|
- **Frontend**: Error boundaries, try-catch with user-friendly messages
|
||||||
|
|
||||||
|
### API Conventions
|
||||||
|
- REST endpoints: `/api/v1/resources`
|
||||||
|
- JSON fields: snake_case
|
||||||
|
- Pagination: `page`, `limit` query params
|
||||||
|
- Response format:
|
||||||
|
```typescript
|
||||||
|
{
|
||||||
|
data: T,
|
||||||
|
meta: { total, page, limit },
|
||||||
|
error?: { code, message }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
- Unit tests for services/hooks (AAA pattern: Arrange-Act-Assert)
|
||||||
|
- Integration tests for API endpoints
|
||||||
|
- E2E tests for critical user flows
|
||||||
|
- Descriptive test names that explain what is tested
|
||||||
|
- Mock external dependencies (use Jest for backend, Vitest for frontend)
|
||||||
|
- Run `npm test` for all tests or target specific files/names
|
||||||
|
|
||||||
|
### Git Workflow
|
||||||
|
- Conventional commits: `feat:`, `fix:`, `refactor:`, `test:`, `docs:`, `chore:`
|
||||||
|
- Keep commits atomic and focused
|
||||||
|
- Write clear commit messages in imperative mood
|
||||||
|
|
||||||
|
### Component Guidelines
|
||||||
|
- Keep components small and focused (< 200 lines)
|
||||||
|
- Use functional components with hooks
|
||||||
|
- Prefer composition over inheritance
|
||||||
|
- Extract complex logic into custom hooks
|
||||||
|
- Use TanStack Query for server state management
|
||||||
|
- Use TanStack Router for routing
|
||||||
|
- Validate props with TypeScript
|
||||||
|
|
||||||
|
### Database
|
||||||
|
- Use TypeORM for SQLite
|
||||||
|
- Define entities with proper relationships
|
||||||
|
- Use migrations for schema changes
|
||||||
|
- Seed data with factory functions
|
||||||
|
- Use TypeORM repositories for data access
|
||||||
|
|
||||||
|
### UI Components
|
||||||
|
- Use shadcn/ui components from `components/ui/`
|
||||||
|
- Use class-variance-authority (CVA) for component variants
|
||||||
|
- Use clsx and tailwind-merge for class composition
|
||||||
|
- Follow shadcn patterns for new component creation
|
||||||
|
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
- Backend: `@nestjs/config` with `.env` files
|
||||||
|
- Frontend: Vite env vars (`VITE_API_URL`)
|
||||||
|
- Never commit secrets to the repository.
|
||||||
|
- Example frontend env: `VITE_API_URL=http://localhost:3000`
|
||||||
|
- Example backend env: `DATABASE_PATH=./data.db`
|
||||||
|
- Store sensitive config in `.env` files
|
||||||
|
- Use `DATABASE_PATH` for SQLite location
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
# Frontend Setup Finalized ✅
|
||||||
|
|
||||||
|
## What's Working
|
||||||
|
|
||||||
|
- ✅ Tailwind CSS v4 (CSS-first approach - no config file needed)
|
||||||
|
- ✅ Shadcn/ui Card component with Tailwind classes
|
||||||
|
- ✅ Responsive grid layout (1/2/3 columns)
|
||||||
|
- ✅ Article data fetching from backend
|
||||||
|
- ✅ Loading, error, and empty states
|
||||||
|
- ✅ TypeScript path aliases (`@/`)
|
||||||
|
- ✅ shadcn/ui configuration (components.json)
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Tailwind CSS (v4 CSS-First)
|
||||||
|
All Tailwind configuration is in `src/index.css` using directives:
|
||||||
|
```css
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
@custom-colors;
|
||||||
|
@dark-mode;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**No `tailwind.config.js` file needed** - this is the v4 CSS-first approach.
|
||||||
|
|
||||||
|
### Shadcn/ui
|
||||||
|
- **components.json** - shadcn/ui configuration file
|
||||||
|
- **components/ui/card.tsx** - Card component with all sub-components
|
||||||
|
- **components/ui/button.tsx** - Button component (installed but unused)
|
||||||
|
- **utils.ts** - `cn()` utility for merging classes
|
||||||
|
|
||||||
|
### TypeScript
|
||||||
|
- Path aliases configured in `tsconfig.json`:
|
||||||
|
```json
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Current Files
|
||||||
|
|
||||||
|
### Core App Files
|
||||||
|
- `src/App.tsx` - Main app component with article grid
|
||||||
|
- `src/main.tsx` - App entry point
|
||||||
|
- `src/lib/api.ts` - Backend API functions
|
||||||
|
- `src/lib/query-client.ts` - TanStack Query configuration
|
||||||
|
|
||||||
|
### UI Components
|
||||||
|
- `src/components/ui/card.tsx` - Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter
|
||||||
|
- `src/components/ui/button.tsx` - Button component (ready to use)
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- `src/index.css` - Tailwind v4 CSS-first configuration
|
||||||
|
- `components.json` - shadcn/ui config
|
||||||
|
- `tsconfig.json` - TypeScript config with path aliases
|
||||||
|
- `vite.config.ts` - Vite configuration
|
||||||
|
|
||||||
|
## Restart Required
|
||||||
|
|
||||||
|
**Restart dev server:**
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
# Press Ctrl+C to stop current server
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
**Refresh browser:** Ctrl+Shift+R (hard refresh)
|
||||||
|
|
||||||
|
## What You Should See
|
||||||
|
|
||||||
|
- Clean white/light gray background
|
||||||
|
- "Placebo.mk" header in large bold text
|
||||||
|
- "Sarcastic news from Macedonia" subtitle
|
||||||
|
- Grid of article cards (responsive: 1/2/3 columns)
|
||||||
|
- Each card has:
|
||||||
|
- White background with border and subtle shadow
|
||||||
|
- Title (truncated to 2 lines)
|
||||||
|
- Excerpt (truncated to 3 lines)
|
||||||
|
- Content preview (truncated to 4 lines)
|
||||||
|
- Footer with date and view count
|
||||||
|
- Hover effect adds shadow to cards
|
||||||
|
|
||||||
|
## Adding More Shadcn/ui Components
|
||||||
|
|
||||||
|
### Using CLI (Recommended)
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npx shadcn@latest add [component-name]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Available Components
|
||||||
|
- `badge` - For article tags
|
||||||
|
- `button` - Interactive elements (already installed)
|
||||||
|
- `input` - Search input
|
||||||
|
- `dialog` - Article detail modal
|
||||||
|
- `dropdown-menu` - Menus and filters
|
||||||
|
- `tabs` - Content organization
|
||||||
|
- `separator` - Visual separation
|
||||||
|
- `skeleton` - Loading placeholder
|
||||||
|
- `pagination` - Navigate articles
|
||||||
|
- `alert` - Notifications
|
||||||
|
|
||||||
|
### Manual Example
|
||||||
|
|
||||||
|
If CLI doesn't work, manual component setup:
|
||||||
|
|
||||||
|
1. Visit https://ui.shadcn.com/docs/components/[component]
|
||||||
|
2. Copy component code
|
||||||
|
3. Create: `frontend/src/components/ui/[component].tsx`
|
||||||
|
4. Import and use: `import { Component } from '@/components/ui/component'`
|
||||||
|
|
||||||
|
## Styling Customization
|
||||||
|
|
||||||
|
### Theme Colors (Edit `src/index.css`)
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%; /* Background color */
|
||||||
|
--foreground: 222.2 84% 4.9%; /* Text color */
|
||||||
|
--primary: 222.2 47.4% 11.2%; /* Primary brand color */
|
||||||
|
--card: 0 0% 100%; /* Card background */
|
||||||
|
/* ... more variables */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tailwind v4 CSS-First Configuration
|
||||||
|
|
||||||
|
The `@theme` directive allows you to customize Tailwind directly in CSS:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@theme {
|
||||||
|
--color-primary: oklch(0.647 0.22 0.23);
|
||||||
|
--font-sans: "Inter", sans-serif;
|
||||||
|
--animate-fade-in: fade-in 0.5s ease-out;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known Limitations (Current Setup)
|
||||||
|
|
||||||
|
- No article detail page (clicking on cards doesn't navigate)
|
||||||
|
- No search functionality
|
||||||
|
- No category/tag filtering
|
||||||
|
- No dark mode toggle
|
||||||
|
- No pagination
|
||||||
|
- No article creation/editing (that's Strapi CMS)
|
||||||
|
|
||||||
|
## Quick Start Checklist
|
||||||
|
|
||||||
|
After restarting dev server:
|
||||||
|
|
||||||
|
- [ ] See styled article cards
|
||||||
|
- [ ] Check responsiveness (resize browser)
|
||||||
|
- [ ] Test dark mode (add toggle later)
|
||||||
|
- [ ] Add more articles in Strapi
|
||||||
|
- [ ] Add shadcn/ui components as needed
|
||||||
|
|
||||||
|
## Adding Article Detail Page
|
||||||
|
|
||||||
|
If you want to add navigation when clicking on cards:
|
||||||
|
|
||||||
|
1. Create `src/components/ArticleDetail.tsx`:
|
||||||
|
```tsx
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import * as api from './lib/api'
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from './components/ui/card'
|
||||||
|
import { Button } from './components/ui/button'
|
||||||
|
|
||||||
|
export function ArticleDetail({ articleId }: { articleId: string }) {
|
||||||
|
const { data, isLoading, error } = useQuery({
|
||||||
|
queryKey: ['article', articleId],
|
||||||
|
queryFn: () => api.fetchArticleById(articleId),
|
||||||
|
enabled: !!articleId,
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isLoading) return <div>Loading...</div>
|
||||||
|
if (error) return <div>Error</div>
|
||||||
|
if (!data) return null
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="max-w-3xl mx-auto">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>{data.title}</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: data.content }} />
|
||||||
|
</CardContent>
|
||||||
|
<Button onClick={() => window.history.back()}>
|
||||||
|
Back to Articles
|
||||||
|
</Button>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Update `App.tsx` to track selected article:
|
||||||
|
```tsx
|
||||||
|
const [selectedArticle, setSelectedArticle] = useState<string | null>(null)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Restart dev server and verify styling works**
|
||||||
|
2. **Add more articles** in Strapi CMS and sync
|
||||||
|
3. **Add shadcn/ui components** as needed
|
||||||
|
4. **Implement article detail view**
|
||||||
|
5. **Add search and filtering**
|
||||||
|
6. **Add dark mode toggle**
|
||||||
|
7. **Add pagination**
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Tailwind CSS v4 Docs](https://tailwindcss.com/docs/installation/using-postcss-css)
|
||||||
|
- [Shadcn/ui Documentation](https://ui.shadcn.com)
|
||||||
|
- [TanStack Query Docs](https://tanstack.com/query/latest)
|
||||||
|
- [Radix UI](https://www.radix-ui.com/)
|
||||||
@@ -0,0 +1,348 @@
|
|||||||
|
# Frontend Setup Complete - TanStack Router + Tailwind 4 ✅
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit:
|
||||||
|
- Home: http://localhost:5173/
|
||||||
|
- Articles: http://localhost:5173/articles
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Tech Stack
|
||||||
|
- **React 19** - UI framework
|
||||||
|
- **TanStack Router** - File-based routing
|
||||||
|
- **TanStack Query** - Server state management
|
||||||
|
- **Tailwind CSS 4** - CSS-first (v4 no config file)
|
||||||
|
- **Shadcn/ui** - Pre-built components
|
||||||
|
- **TypeScript** - Type safety
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ └── ui/ # shadcn/ui components
|
||||||
|
│ │ ├── button.tsx
|
||||||
|
│ │ └── card.tsx
|
||||||
|
│ ├── lib/
|
||||||
|
│ │ ├── api.ts # Backend API functions
|
||||||
|
│ │ ├── query-client.ts # TanStack Query config
|
||||||
|
│ │ └── utils.ts # cn() utility
|
||||||
|
│ ├── routes/ # TanStack Router routes
|
||||||
|
│ │ ├── root.tsx # Root layout
|
||||||
|
│ │ ├── index.tsx # Home page
|
||||||
|
│ │ └── articles.tsx # Articles list
|
||||||
|
│ ├── styles.css # Tailwind CSS + theme
|
||||||
|
│ └── main.tsx # App entry
|
||||||
|
├── components.json # shadcn/ui config
|
||||||
|
├── vite.config.ts # Vite + Tailwind CSS plugin
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pages & Routes
|
||||||
|
|
||||||
|
### Home Page (`/`)
|
||||||
|
- Welcome message about Placebo.mk
|
||||||
|
- "Welcome" card with intro
|
||||||
|
- "Get Started" card with feature list
|
||||||
|
- Centered layout with max-width container
|
||||||
|
|
||||||
|
### Articles Page (`/articles`)
|
||||||
|
- Grid of article cards (responsive: 1/2/3 columns)
|
||||||
|
- Each card shows:
|
||||||
|
- Title (truncated to 2 lines)
|
||||||
|
- Excerpt (truncated to 3 lines)
|
||||||
|
- Content preview (truncated to 4 lines)
|
||||||
|
- Creation date
|
||||||
|
- View count
|
||||||
|
- Loading state
|
||||||
|
- Error state
|
||||||
|
- Empty state when no articles
|
||||||
|
|
||||||
|
### Root Layout
|
||||||
|
- Header with navigation (Home, Articles links)
|
||||||
|
- Main content area with `<Outlet />`
|
||||||
|
- Footer with copyright
|
||||||
|
- SEO meta tags
|
||||||
|
- Tailwind CSS import
|
||||||
|
|
||||||
|
## Components
|
||||||
|
|
||||||
|
### Card (`src/components/ui/card.tsx`)
|
||||||
|
- Card - Main container
|
||||||
|
- CardHeader - Header section
|
||||||
|
- CardTitle - Title (h3 element)
|
||||||
|
- CardDescription - Description (p element)
|
||||||
|
- CardContent - Content area
|
||||||
|
- CardFooter - Footer area
|
||||||
|
|
||||||
|
### Button (`src/components/ui/button.tsx`)
|
||||||
|
- Variants: default, destructive, outline, secondary, ghost, link
|
||||||
|
- Sizes: default, sm, lg, icon
|
||||||
|
- Uses shadcn/ui theming system
|
||||||
|
- Ready to use (not used yet)
|
||||||
|
|
||||||
|
## Utilities
|
||||||
|
|
||||||
|
### cn() Function (`src/lib/utils.ts`)
|
||||||
|
```tsx
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
// Merge Tailwind classes with conditional logic
|
||||||
|
<div className={cn(
|
||||||
|
'base-class',
|
||||||
|
isActive && 'active-class',
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Styling
|
||||||
|
|
||||||
|
### Tailwind CSS 4 (CSS-First)
|
||||||
|
Configuration in `src/styles.css`:
|
||||||
|
```css
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-primary: oklch(0.647 0.22 0.23);
|
||||||
|
--font-sans: "Inter", sans-serif;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shadcn/ui Theme
|
||||||
|
CSS variables in `src/styles.css`:
|
||||||
|
- `--background` - Background color
|
||||||
|
- `--foreground` - Text color
|
||||||
|
- `--card` - Card background
|
||||||
|
- `--primary` - Primary brand color
|
||||||
|
- `--muted` - Muted text color
|
||||||
|
- Dark mode support with `.dark` class
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
### API Layer (`src/lib/api.ts`)
|
||||||
|
- `fetchArticles()` - Get all articles with filters
|
||||||
|
- `fetchArticleById()` - Get single article by ID
|
||||||
|
- `fetchArticleBySlug()` - Get article by slug
|
||||||
|
|
||||||
|
### TanStack Query
|
||||||
|
- Automatic caching
|
||||||
|
- Refetch on window focus (disabled)
|
||||||
|
- Stale time: 5 minutes
|
||||||
|
- Retry: 1 attempt
|
||||||
|
- Query client shared across app
|
||||||
|
|
||||||
|
## Backend Integration
|
||||||
|
|
||||||
|
### API Configuration
|
||||||
|
```typescript
|
||||||
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Environment Variables
|
||||||
|
Create `frontend/.env`:
|
||||||
|
```bash
|
||||||
|
VITE_API_URL=http://localhost:3000/api/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding New Pages
|
||||||
|
|
||||||
|
### Method 1: File-Based Routes (Current)
|
||||||
|
Create new route file in `src/routes/`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/routes/about.tsx
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/about')({
|
||||||
|
component: () => <div>About Page</div>,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Add to `src/main.tsx`:
|
||||||
|
```tsx
|
||||||
|
import { aboutRoute } from './routes/about'
|
||||||
|
|
||||||
|
const routeTree = rootRoute.addChildren([indexRoute, articlesRoute, aboutRoute])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: Using TanStack Router
|
||||||
|
Create routes dynamically in existing route files.
|
||||||
|
|
||||||
|
## Adding Shadcn/ui Components
|
||||||
|
|
||||||
|
### Using CLI (Recommended)
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npx shadcn@latest add [component-name]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Components
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest add badge # Article tags
|
||||||
|
npx shadcn@latest add input # Search box
|
||||||
|
npx shadcn@latest add dialog # Article detail modal
|
||||||
|
npx shadcn@latest add tabs # Content organization
|
||||||
|
npx shadcn@latest add separator # Visual separation
|
||||||
|
npx shadcn@latest add skeleton # Loading placeholder
|
||||||
|
npx shadcn@latest add pagination # Article list navigation
|
||||||
|
npx shadcn@latest add dropdown-menu # Menus and filters
|
||||||
|
npx shadcn@latest add select # Dropdown selectors
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Changing Brand Colors
|
||||||
|
Edit `src/styles.css`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@theme {
|
||||||
|
--color-primary: oklch(0.7 0.5 0.2); /* Your brand color */
|
||||||
|
--font-sans: "Your Font", sans-serif;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dark Mode
|
||||||
|
Add toggle button to header:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/routes/root.tsx
|
||||||
|
const toggleDarkMode = () => {
|
||||||
|
document.documentElement.classList.toggle('dark')
|
||||||
|
localStorage.setItem('theme',
|
||||||
|
document.documentElement.classList.contains('dark') ? 'dark' : 'light'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// In component
|
||||||
|
<button onClick={toggleDarkMode}>Toggle Theme</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
Edit `src/styles.css`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@theme {
|
||||||
|
--font-sans: "Your Custom Font", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Routes Not Loading
|
||||||
|
1. Check route files exist in `src/routes/`
|
||||||
|
2. Check exports: `export const Route = createFileRoute(...)`
|
||||||
|
3. Check route tree in `src/main.tsx`
|
||||||
|
4. Restart dev server
|
||||||
|
|
||||||
|
### Styles Not Applying
|
||||||
|
1. Check `src/styles.css` has Tailwind directives
|
||||||
|
2. Check `vite.config.ts` has `tailwindcss()` plugin
|
||||||
|
3. Restart dev server
|
||||||
|
4. Hard refresh browser (Ctrl+Shift+R)
|
||||||
|
|
||||||
|
### Component Imports Failing
|
||||||
|
1. Check `tsconfig.json` has `@/` paths
|
||||||
|
2. Check import paths are correct: `@/components/ui/component`
|
||||||
|
3. Restart TypeScript server (VSCode: Cmd+Shift+P)
|
||||||
|
|
||||||
|
### Data Not Fetching
|
||||||
|
1. Check backend is running: `http://localhost:3000`
|
||||||
|
2. Check `.env` file exists and has correct API URL
|
||||||
|
3. Check browser network tab for failed requests
|
||||||
|
4. Check CORS in backend (`backend/src/main.ts`)
|
||||||
|
|
||||||
|
### TypeScript Errors
|
||||||
|
1. Run: `npm run type-check`
|
||||||
|
2. Restart TypeScript server (VSCode)
|
||||||
|
3. Clear Vite cache: `rm -rf node_modules/.vite`
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
1. **Code Splitting**: Use lazy() for large components
|
||||||
|
2. **Image Optimization**: Add loading="lazy" to article images
|
||||||
|
3. **Query Caching**: TanStack Query caches automatically
|
||||||
|
4. **Bundle Size**: Run `npm run build` and check size
|
||||||
|
|
||||||
|
## Development Workflow
|
||||||
|
|
||||||
|
### Adding New Article
|
||||||
|
1. Create article in Strapi CMS: http://localhost:1337/admin
|
||||||
|
2. Publish article
|
||||||
|
3. Sync to backend: `curl -X POST http://localhost:3000/api/v1/webhooks/strapi/sync/all`
|
||||||
|
4. Refresh frontend to see new article
|
||||||
|
|
||||||
|
### Running All Services
|
||||||
|
```bash
|
||||||
|
# Terminal 1 - Backend
|
||||||
|
cd backend
|
||||||
|
npm run start:dev
|
||||||
|
|
||||||
|
# Terminal 2 - Frontend
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Terminal 3 - Strapi CMS (optional)
|
||||||
|
cd cms/cms
|
||||||
|
npm run develop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Manual Testing
|
||||||
|
```bash
|
||||||
|
# Test API
|
||||||
|
curl http://localhost:3000/api/v1/articles
|
||||||
|
|
||||||
|
# Test frontend
|
||||||
|
npm run dev
|
||||||
|
# Open http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
### Type Checking
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run type-check
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. Add article detail page with `$id` parameter
|
||||||
|
2. Add search input for filtering articles
|
||||||
|
3. Add category/tag filtering
|
||||||
|
4. Add pagination for article lists
|
||||||
|
5. Add dark mode toggle
|
||||||
|
6. Add "about" page with site information
|
||||||
|
7. Add "contact" page or form
|
||||||
|
8. Add article comments system
|
||||||
|
9. Add social sharing buttons
|
||||||
|
10. Add analytics for article views
|
||||||
|
|
||||||
|
## Files to Edit
|
||||||
|
|
||||||
|
| File | Purpose |
|
||||||
|
|-------|---------|
|
||||||
|
| `src/styles.css` | Theme colors, fonts, CSS variables |
|
||||||
|
| `vite.config.ts` | Vite plugins, path aliases |
|
||||||
|
| `components.json` | shadcn/ui configuration |
|
||||||
|
| `tsconfig.json` | TypeScript paths |
|
||||||
|
| `.env` | Environment variables |
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [TanStack Router Docs](https://tanstack.com/router/latest)
|
||||||
|
- [Tailwind CSS 4 Docs](https://tailwindcss.com/docs/installation/using-postcss)
|
||||||
|
- [Shadcn/ui Documentation](https://ui.shadcn.com)
|
||||||
|
- [TanStack Query Docs](https://tanstack.com/query/latest)
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Frontend Setup Complete ✅
|
||||||
|
|
||||||
|
## What's Ready
|
||||||
|
|
||||||
|
### Frontend with Shadcn/ui
|
||||||
|
- Clean, minimalistic design
|
||||||
|
- Responsive grid layout for articles
|
||||||
|
- Loading and error states
|
||||||
|
- Card-based article display
|
||||||
|
- Dark mode support (configured)
|
||||||
|
- TypeScript + Tailwind CSS
|
||||||
|
|
||||||
|
### Components Available
|
||||||
|
- `Card` - Display article content
|
||||||
|
- `Button` - Interactive elements
|
||||||
|
|
||||||
|
### Routing
|
||||||
|
- Single page app with all articles on home page
|
||||||
|
- Easy to extend with more routes when needed
|
||||||
|
|
||||||
|
## How to Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit: http://localhost:5173
|
||||||
|
|
||||||
|
## Adding More Components
|
||||||
|
|
||||||
|
Use shadcn/ui CLI:
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest add [component]
|
||||||
|
```
|
||||||
|
|
||||||
|
Available components: badge, input, dialog, dropdown-menu, select, tabs, etc.
|
||||||
|
|
||||||
|
See `SHADCN_SETUP.md` for full documentation.
|
||||||
|
|
||||||
|
## Current State
|
||||||
|
|
||||||
|
- **Home page**: Displays all published articles as cards
|
||||||
|
- **Backend**: Fetches from `http://localhost:3000/api/v1/articles`
|
||||||
|
- **Strapi integration**: Articles sync from Strapi CMS
|
||||||
|
- **Articles**: Currently showing 1 article "first article"
|
||||||
|
|
||||||
|
## Next Steps for You
|
||||||
|
|
||||||
|
1. Add more articles in Strapi CMS
|
||||||
|
2. Run sync: `curl -X POST http://localhost:3000/api/v1/webhooks/strapi/sync/all`
|
||||||
|
3. Refresh frontend to see new articles
|
||||||
|
4. Add shadcn/ui components as needed:
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest add badge input dialog
|
||||||
|
```
|
||||||
|
|
||||||
|
## Design Customization
|
||||||
|
|
||||||
|
Edit `frontend/src/index.css`:
|
||||||
|
```css
|
||||||
|
--primary: 222.2 47.4% 11.2%; /* Primary color */
|
||||||
|
--background: 0 0% 100%; /* Background */
|
||||||
|
--foreground: 222.2 84% 4.9%; /* Text color */
|
||||||
|
```
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# Frontend Setup - Working ✅
|
||||||
|
|
||||||
|
## Current Status
|
||||||
|
|
||||||
|
Frontend is working with:
|
||||||
|
- ✅ React 19
|
||||||
|
- ✅ TanStack Query for data fetching from backend
|
||||||
|
- ✅ Inline styles (currently working)
|
||||||
|
- ✅ Article grid layout
|
||||||
|
- ✅ Loading and error states
|
||||||
|
- ✅ Shadcn/ui components installed but not configured yet
|
||||||
|
|
||||||
|
## Running the App
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit: http://localhost:5173
|
||||||
|
|
||||||
|
## Current Data Flow
|
||||||
|
|
||||||
|
1. Frontend fetches from: `http://localhost:3000/api/v1/articles`
|
||||||
|
2. Backend (NestJS) returns articles from SQLite
|
||||||
|
3. Articles synced from Strapi CMS
|
||||||
|
|
||||||
|
## Next Steps for Styling
|
||||||
|
|
||||||
|
### Option 1: Keep Inline Styles (Simple)
|
||||||
|
Continue using inline styles in `App.tsx` - currently working fine.
|
||||||
|
|
||||||
|
### Option 2: Set Up Tailwind CSS (Recommended)
|
||||||
|
If you want to use shadcn/ui components properly:
|
||||||
|
|
||||||
|
1. Install shadcn CLI:
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest init
|
||||||
|
```
|
||||||
|
This will configure Tailwind properly for your project.
|
||||||
|
|
||||||
|
2. Add components:
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest add button card badge
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Use components in `App.tsx`
|
||||||
|
```tsx
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding More Articles
|
||||||
|
|
||||||
|
In Strapi CMS:
|
||||||
|
1. Create new article
|
||||||
|
2. Publish it
|
||||||
|
3. Sync to backend:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/webhooks/strapi/sync/all
|
||||||
|
```
|
||||||
|
4. Refresh frontend
|
||||||
|
|
||||||
|
## Services Running
|
||||||
|
|
||||||
|
- **Backend**: http://localhost:3000/api/v1
|
||||||
|
- **Frontend**: http://localhost:5173
|
||||||
|
- **Strapi CMS**: http://localhost:1337/admin (if running)
|
||||||
|
|
||||||
|
## Known Files
|
||||||
|
|
||||||
|
- `frontend/src/App.tsx` - Main app with article list
|
||||||
|
- `frontend/src/lib/api.ts` - API functions
|
||||||
|
- `frontend/src/lib/query-client.ts` - TanStack Query client
|
||||||
|
- `frontend/src/components/ui/card.tsx` - Card component
|
||||||
|
- `frontend/src/components/ui/button.tsx` - Button component
|
||||||
|
|
||||||
|
## To Revert to Inline Styles Only
|
||||||
|
|
||||||
|
If you want to remove Tailwind dependencies later:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm uninstall tailwindcss tailwindcss-animate clsx tailwind-merge class-variance-authority lucide-react @radix-ui/react-slot
|
||||||
|
rm -rf src/components/ui
|
||||||
|
rm components.json tailwind.config.js
|
||||||
|
```
|
||||||
|
|
||||||
|
Then update `App.tsx` to use inline styles only.
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Strapi Integration - Complete ✅
|
||||||
|
|
||||||
|
## What Was Added
|
||||||
|
|
||||||
|
### Backend Updates
|
||||||
|
1. **Fixed SQLite enum issue**: Changed Article.status from enum type to text type (SQLite doesn't support enum)
|
||||||
|
2. **Fixed entity initialization order**: Moved Author and Category definitions before Article to avoid circular reference errors
|
||||||
|
3. **Fixed SQLite array issue**: Changed tags from simple-array to text type (SQLite doesn't support arrays)
|
||||||
|
4. **Added Strapi integration**:
|
||||||
|
- `strapi.service.ts` - Service for syncing content from Strapi
|
||||||
|
- `strapi.controller.ts` - Webhook endpoint controller
|
||||||
|
- `strapi.module.ts` - Module containing Strapi functionality
|
||||||
|
5. **Installed dependencies**: `@nestjs/axios` for HTTP requests to Strapi API
|
||||||
|
6. **Updated CORS**: Added Strapi URL to allowed origins
|
||||||
|
7. **Updated environment variables**: Added STRAPI_URL and STRAPI_API_TOKEN
|
||||||
|
|
||||||
|
### Database Schema
|
||||||
|
- Authors table with id, name, slug, bio, avatar, isActive, timestamps
|
||||||
|
- Categories table with id, name, slug, description, parentId, order, timestamps
|
||||||
|
- Articles table with id, title, content, excerpt, slug, featuredImage, tags (text), status (text), views, strapiId, authorId, categoryId, timestamps
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- Updated `backend/src/app.module.ts` to include StrapiModule
|
||||||
|
- Updated `backend/.env.example` with Strapi configuration
|
||||||
|
- Created `STRAPI_INTEGRATION.md` with detailed setup instructions
|
||||||
|
- Updated `SETUP_COMPLETE.md` with Strapi integration info
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Start all services**:
|
||||||
|
```bash
|
||||||
|
# Terminal 1 - Backend
|
||||||
|
cd backend
|
||||||
|
npm run start:dev
|
||||||
|
|
||||||
|
# Terminal 2 - Frontend
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# Terminal 3 - Strapi
|
||||||
|
cd cms/cms
|
||||||
|
npm run develop
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Configure Strapi** (see STRAPI_INTEGRATION.md):
|
||||||
|
- Create content types: Article, Author, Category, Tag
|
||||||
|
- Create API token
|
||||||
|
- Configure webhook pointing to backend
|
||||||
|
- Add API token to backend `.env`
|
||||||
|
|
||||||
|
3. **Test the integration**:
|
||||||
|
- Create an article in Strapi
|
||||||
|
- Publish it
|
||||||
|
- Check backend: `curl http://localhost:3000/api/v1/articles`
|
||||||
|
- Verify it appears in frontend
|
||||||
|
|
||||||
|
## Webhook Flow
|
||||||
|
|
||||||
|
1. Content created/updated in Strapi → 2. Strapi sends webhook to backend → 3. Backend fetches content from Strapi API → 4. Backend syncs to SQLite database → 5. Frontend queries backend API
|
||||||
|
|
||||||
|
## API Endpoints Added
|
||||||
|
|
||||||
|
- `POST /api/v1/webhooks/strapi/article` - Handle Strapi webhooks
|
||||||
|
- `POST /api/v1/webhooks/strapi/sync/all` - Manual full sync trigger
|
||||||
|
|
||||||
|
## Fixed Issues
|
||||||
|
|
||||||
|
✅ SQLite enum type not supported - changed to text
|
||||||
|
✅ Circular entity reference - reordered definitions
|
||||||
|
✅ Frontend routeTree not exported - added export
|
||||||
|
✅ All TypeScript errors resolved
|
||||||
|
✅ All linting errors resolved
|
||||||
|
|
||||||
|
All services are ready to run!
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
# TanStack Router + Tailwind 4 Setup Complete ✅
|
||||||
|
|
||||||
|
## What's Been Configured
|
||||||
|
|
||||||
|
### 1. TanStack Router with File-Based Routing
|
||||||
|
- **Routes directory**: `frontend/src/routes/`
|
||||||
|
- **Root route**: `root.tsx` - Layout with header, footer, and outlet
|
||||||
|
- **Home route**: `index.tsx` - Welcome page with getting started cards
|
||||||
|
- **Articles route**: `articles.tsx` - Article list with card grid
|
||||||
|
- **Router configuration**: Uses `createRouter` with route tree
|
||||||
|
|
||||||
|
### 2. Tailwind CSS 4 (CSS-First)
|
||||||
|
- **Configuration**: In `src/styles.css` using `@import` and `@theme` directives
|
||||||
|
- **Vite integration**: `tailwindcss()` plugin in `vite.config.ts`
|
||||||
|
- **No config file needed**: Tailwind 4 CSS-first doesn't use `tailwind.config.js`
|
||||||
|
- **Shadcn/ui compatible**: CSS variables for theming
|
||||||
|
|
||||||
|
### 3. Routing Structure
|
||||||
|
```
|
||||||
|
frontend/src/routes/
|
||||||
|
├── root.tsx # Main layout (header, footer, <Outlet />)
|
||||||
|
├── index.tsx # Home page (/)
|
||||||
|
└── articles.tsx # Articles list (/articles)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Page Templates
|
||||||
|
|
||||||
|
#### Root Layout (`root.tsx`)
|
||||||
|
- Header with navigation (Home, Articles)
|
||||||
|
- Main content area with `<Outlet />`
|
||||||
|
- Footer with copyright
|
||||||
|
- Meta tags for SEO
|
||||||
|
- CSS import for Tailwind
|
||||||
|
|
||||||
|
#### Home Page (`index.tsx`)
|
||||||
|
- Welcome cards with:
|
||||||
|
- "Welcome" - Introduction
|
||||||
|
- "Get Started" - Feature list
|
||||||
|
- Centered layout with max-width container
|
||||||
|
|
||||||
|
#### Articles Page (`articles.tsx`)
|
||||||
|
- Grid of article cards (responsive: 1/2/3 columns)
|
||||||
|
- Loading state
|
||||||
|
- Error state
|
||||||
|
- Empty state (no articles)
|
||||||
|
- Each card shows:
|
||||||
|
- Featured image (if available)
|
||||||
|
- Title (truncated to 2 lines)
|
||||||
|
- Excerpt (truncated to 3 lines)
|
||||||
|
- Content preview (truncated to 4 lines)
|
||||||
|
- Date and view count
|
||||||
|
|
||||||
|
## How It Works
|
||||||
|
|
||||||
|
### 1. Route Configuration
|
||||||
|
```tsx
|
||||||
|
// src/routes/root.tsx
|
||||||
|
import { createRootRoute, Link, Outlet } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const rootRoute = createRootRoute({
|
||||||
|
head: () => ({
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
title: 'Placebo.mk - Sarcastic News from Macedonia',
|
||||||
|
description: 'Latest news and articles from Macedonia with a sarcastic twist',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
links: [{ rel: 'stylesheet', href: '../../styles.css' }],
|
||||||
|
}),
|
||||||
|
component: () => (
|
||||||
|
<div>
|
||||||
|
<header>...</header>
|
||||||
|
<main><Outlet /></main>
|
||||||
|
<footer>...</footer>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Route Tree
|
||||||
|
```tsx
|
||||||
|
// src/main.tsx
|
||||||
|
import { createRouter } from '@tanstack/react-router'
|
||||||
|
import { rootRoute } from './routes/root'
|
||||||
|
import { indexRoute } from './routes/index'
|
||||||
|
import { articlesRoute } from './routes/articles'
|
||||||
|
|
||||||
|
const routeTree = rootRoute.addChildren([indexRoute, articlesRoute])
|
||||||
|
const router = createRouter({ routeTree, context: { queryClient } })
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. File-Based Routes
|
||||||
|
```tsx
|
||||||
|
// src/routes/index.tsx
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/')({
|
||||||
|
component: () => <div>Home</div>,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/routes/articles.tsx
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/articles')({
|
||||||
|
component: () => <div>Articles</div>,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Tailwind 4 CSS-First Setup
|
||||||
|
|
||||||
|
#### In `src/styles.css`:
|
||||||
|
```css
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-primary: oklch(0.647 0.22 0.23);
|
||||||
|
--font-sans: "Inter", sans-serif;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### In `vite.config.ts`:
|
||||||
|
```ts
|
||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from 'tailwindcss'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react(), tailwindcss()],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the App
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Visit:
|
||||||
|
- Home: http://localhost:5173/
|
||||||
|
- Articles: http://localhost:5173/articles
|
||||||
|
|
||||||
|
## Adding New Routes
|
||||||
|
|
||||||
|
### Method 1: File-Based Routes (Recommended)
|
||||||
|
Create a new file in `src/routes/`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// src/routes/article-detail.tsx
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import * as api from '../lib/api'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/articles/$id')({
|
||||||
|
component: () => {
|
||||||
|
const { id } = Route.useParams()
|
||||||
|
const { data } = useQuery({
|
||||||
|
queryKey: ['article', id],
|
||||||
|
queryFn: () => api.fetchArticleById(id),
|
||||||
|
})
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>{data?.title}</h1>
|
||||||
|
<div dangerouslySetInnerHTML={{ __html: data?.content || '' }} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Then add to `src/main.tsx`:
|
||||||
|
```tsx
|
||||||
|
import { articleDetailRoute } from './routes/article-detail'
|
||||||
|
|
||||||
|
const routeTree = rootRoute.addChildren([indexRoute, articlesRoute, articleDetailRoute])
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method 2: Using Link for Navigation
|
||||||
|
```tsx
|
||||||
|
import { Link } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
// In your component
|
||||||
|
<Link to="/article/123">Read Article</Link>
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding Shadcn/ui Components
|
||||||
|
|
||||||
|
### Using the CLI (Recommended)
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npx shadcn@latest add [component-name]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Common Components to Add
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest add button # (already installed)
|
||||||
|
npx shadcn@latest add badge # For article tags
|
||||||
|
npx shadcn@latest add input # For search
|
||||||
|
npx shadcn@latest add dialog # For article details
|
||||||
|
npx shadcn@latest add dropdown-menu
|
||||||
|
npx shadcn@latest add select
|
||||||
|
npx shadcn@latest add tabs # For content organization
|
||||||
|
npx shadcn@latest add separator # For visual separation
|
||||||
|
npx shadcn@latest add skeleton # For loading states
|
||||||
|
npx shadcn@latest add pagination # For article list navigation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Components in Routes
|
||||||
|
```tsx
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/articles')({
|
||||||
|
component: () => (
|
||||||
|
<div>
|
||||||
|
<Button variant="default">Click me</Button>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Theme Colors
|
||||||
|
|
||||||
|
Edit `src/styles.css`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@theme {
|
||||||
|
--color-primary: oklch(0.647 0.22 0.23);
|
||||||
|
--color-secondary: oklch(0.8 0.15 0.1);
|
||||||
|
--font-sans: "Your Font", sans-serif;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Fonts
|
||||||
|
|
||||||
|
Edit `src/styles.css`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
@theme {
|
||||||
|
--font-sans: "Inter", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply font-sans;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dark Mode
|
||||||
|
|
||||||
|
Add dark mode toggle to header:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export const rootRoute = createRootRoute({
|
||||||
|
component: () => {
|
||||||
|
const toggleDarkMode = () => {
|
||||||
|
document.documentElement.classList.toggle('dark')
|
||||||
|
localStorage.setItem('theme',
|
||||||
|
document.documentElement.classList.contains('dark') ? 'dark' : 'light'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<header>
|
||||||
|
<button onClick={toggleDarkMode}>
|
||||||
|
Toggle Theme
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<main><Outlet /></main>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ └── ui/ # shadcn/ui components
|
||||||
|
│ ├── lib/
|
||||||
|
│ │ ├── api.ts # Backend API functions
|
||||||
|
│ │ ├── query-client.ts # TanStack Query configuration
|
||||||
|
│ │ └── utils.ts # cn() utility (for shadcn/ui)
|
||||||
|
│ ├── routes/ # TanStack Router routes
|
||||||
|
│ │ ├── root.tsx # Root layout
|
||||||
|
│ │ ├── index.tsx # Home page
|
||||||
|
│ │ └── articles.tsx # Articles list
|
||||||
|
│ ├── styles.css # Tailwind CSS + theme
|
||||||
|
│ └── main.tsx # App entry point
|
||||||
|
├── components.json # shadcn/ui configuration
|
||||||
|
├── vite.config.ts # Vite + Tailwind CSS plugin
|
||||||
|
├── tailwind.config.js # (Not used - CSS-first approach)
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Routes Not Working
|
||||||
|
|
||||||
|
1. Check that route files are in `src/routes/`
|
||||||
|
2. Check export: `export const Route = createFileRoute(...)`
|
||||||
|
3. Check imports in `src/main.tsx`
|
||||||
|
4. Restart dev server
|
||||||
|
|
||||||
|
### Styles Not Applying
|
||||||
|
|
||||||
|
1. Check `src/styles.css` has `@import "tailwindcss"`
|
||||||
|
2. Check `vite.config.ts` has `tailwindcss()` plugin
|
||||||
|
3. Restart dev server
|
||||||
|
4. Hard refresh browser (Ctrl+Shift+R)
|
||||||
|
|
||||||
|
### Component Imports Not Working
|
||||||
|
|
||||||
|
1. Check `tsconfig.json` has `@/` paths configured
|
||||||
|
2. Check `vite.config.ts` has resolve alias for `@/`
|
||||||
|
3. Restart TypeScript server (VSCode: Cmd+Shift+P → "TypeScript: Restart TS Server")
|
||||||
|
|
||||||
|
### TanStack Query Not Fetching
|
||||||
|
|
||||||
|
1. Check backend is running: `http://localhost:3000`
|
||||||
|
2. Check `.env` has correct API URL
|
||||||
|
3. Check browser network tab for failed requests
|
||||||
|
4. Check CORS configuration in backend
|
||||||
|
|
||||||
|
## Best Practices
|
||||||
|
|
||||||
|
### 1. Route Organization
|
||||||
|
- Keep routes in `src/routes/` directory
|
||||||
|
- Use file-based routing for simple pages
|
||||||
|
- Use nested routes for related content
|
||||||
|
|
||||||
|
### 2. Component Reusability
|
||||||
|
- Create reusable components in `src/components/`
|
||||||
|
- Use shadcn/ui for consistent styling
|
||||||
|
- Extract common patterns (loading states, error states)
|
||||||
|
|
||||||
|
### 3. Data Fetching
|
||||||
|
- Use TanStack Query for all server state
|
||||||
|
- Implement proper loading and error states
|
||||||
|
- Use query keys consistently
|
||||||
|
|
||||||
|
### 4. Performance
|
||||||
|
- Use `lazy()` for code splitting large components
|
||||||
|
- Implement image lazy loading
|
||||||
|
- Use `defer` for non-critical resources
|
||||||
|
|
||||||
|
### 5. SEO
|
||||||
|
- Add meta tags to root route
|
||||||
|
- Add Open Graph tags
|
||||||
|
- Implement structured data
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Restart dev server** to load new configuration
|
||||||
|
2. **Visit home page**: http://localhost:5173/
|
||||||
|
3. **Visit articles page**: http://localhost:5173/articles
|
||||||
|
4. **Add article detail page** with `$id` parameter
|
||||||
|
5. **Add shadcn/ui components** as needed
|
||||||
|
6. **Add dark mode toggle**
|
||||||
|
7. **Add search functionality**
|
||||||
|
8. **Add category/tag filtering**
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [TanStack Router Docs](https://tanstack.com/router/latest/docs/framework/react/start/file-based-routing)
|
||||||
|
- [Tailwind CSS 4 Docs](https://tailwindcss.com/docs/installation/using-postcss)
|
||||||
|
- [Shadcn/ui Documentation](https://ui.shadcn.com)
|
||||||
|
- [TanStack Query Docs](https://tanstack.com/query/latest)
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
# Placebo.mk - Basic Structure Complete
|
||||||
|
|
||||||
|
## ✅ What's Been Built
|
||||||
|
|
||||||
|
### Backend (NestJS)
|
||||||
|
- [x] NestJS project initialized with TypeScript
|
||||||
|
- [x] TypeORM configured with SQLite database
|
||||||
|
- [x] Entities created: Article, Author, Category
|
||||||
|
- [x] Articles module with full CRUD operations
|
||||||
|
- [x] DTOs with validation (CreateArticleDto, UpdateArticleDto, FindArticlesDto)
|
||||||
|
- [x] Service layer with business logic
|
||||||
|
- [x] REST API endpoints
|
||||||
|
- [x] CORS configured for frontend and Strapi
|
||||||
|
- [x] Strapi integration service with webhook support
|
||||||
|
- [x] HTTP client for fetching Strapi content
|
||||||
|
- [x] Content sync from Strapi to local database
|
||||||
|
- [x] Environment variables setup
|
||||||
|
- [x] Type checking configured
|
||||||
|
- [x] Linting configured
|
||||||
|
|
||||||
|
### Frontend (TanStack)
|
||||||
|
- [x] Vite + React + TypeScript project initialized
|
||||||
|
- [x] TanStack Router configured
|
||||||
|
- [x] TanStack Query configured
|
||||||
|
- [x] Basic routes created: home, articles list, article detail
|
||||||
|
- [x] API client with TypeScript interfaces
|
||||||
|
- [x] Custom hooks for data fetching
|
||||||
|
- [x] Environment variables setup
|
||||||
|
- [x] Type checking configured
|
||||||
|
- [x] Linting configured
|
||||||
|
|
||||||
|
### CMS (Strapi)
|
||||||
|
- [x] Strapi installed in cms/cms/ directory
|
||||||
|
- [x] Project initialized with TypeScript
|
||||||
|
|
||||||
|
### Project Structure
|
||||||
|
- [x] Monorepo structure with backend, frontend, cms directories
|
||||||
|
- [x] .gitignore for all projects
|
||||||
|
- [x] AGENTS.md with comprehensive guidelines
|
||||||
|
- [x] STRUCTURE.md with project overview
|
||||||
|
- [x] STRAPI_INTEGRATION.md with setup instructions
|
||||||
|
- [x] Environment variable templates (.env.example)
|
||||||
|
|
||||||
|
## 🚀 Ready to Use
|
||||||
|
|
||||||
|
### Backend
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
cp .env.example .env
|
||||||
|
# Edit .env to add STRAPI_API_TOKEN
|
||||||
|
npm run start:dev
|
||||||
|
# API available at http://localhost:3000/api/v1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
cp .env.example .env
|
||||||
|
npm run dev
|
||||||
|
# App available at http://localhost:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
### Strapi CMS
|
||||||
|
```bash
|
||||||
|
cd cms/cms
|
||||||
|
npm run develop
|
||||||
|
# Admin panel available at http://localhost:1337/admin
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📦 Strapi Integration
|
||||||
|
|
||||||
|
The backend now includes:
|
||||||
|
- **StrapiService**: Fetches and syncs content from Strapi
|
||||||
|
- **StrapiController**: Handles webhooks from Strapi
|
||||||
|
- **Manual sync endpoint**: POST `/api/v1/webhooks/strapi/sync/all`
|
||||||
|
- **Webhook endpoint**: POST `/api/v1/webhooks/strapi/article`
|
||||||
|
|
||||||
|
### Setup Steps:
|
||||||
|
1. Create content types in Strapi: Article, Author, Category, Tag (see STRAPI_INTEGRATION.md)
|
||||||
|
2. Create API token in Strapi admin panel
|
||||||
|
3. Add token to backend `.env` as `STRAPI_API_TOKEN`
|
||||||
|
4. Configure webhook in Strapi to point to backend
|
||||||
|
5. Start all three services
|
||||||
|
|
||||||
|
## 🔜 Next Steps
|
||||||
|
|
||||||
|
1. **Configure Strapi content types** following STRAPI_INTEGRATION.md
|
||||||
|
2. **Create webhook** in Strapi for automatic sync
|
||||||
|
3. **Implement frontend UI components** for displaying articles
|
||||||
|
4. **Add authentication** for the CMS
|
||||||
|
5. **Set up image handling** with Strapi media library
|
||||||
|
6. **Implement search** and filtering in the frontend
|
||||||
|
7. **Add analytics** for article views
|
||||||
|
8. **Set up deployment** for all three services
|
||||||
|
|
||||||
|
## 📝 API Endpoints Available
|
||||||
|
|
||||||
|
### Articles
|
||||||
|
- `GET /api/v1/articles` - List articles with pagination and filters
|
||||||
|
- `GET /api/v1/articles/:id` - Get article by ID
|
||||||
|
- `GET /api/v1/articles/slug/:slug` - Get article by slug
|
||||||
|
- `POST /api/v1/articles` - Create article (for Strapi sync)
|
||||||
|
- `PUT /api/v1/articles/:id` - Update article
|
||||||
|
- `DELETE /api/v1/articles/:id` - Delete article
|
||||||
|
|
||||||
|
### Webhooks & Sync
|
||||||
|
- `POST /api/v1/webhooks/strapi/article` - Handle Strapi webhooks
|
||||||
|
- `POST /api/v1/webhooks/strapi/sync/all` - Trigger full sync
|
||||||
|
|
||||||
|
All article endpoints support query parameters:
|
||||||
|
- `category` - Filter by category slug
|
||||||
|
- `author` - Filter by author slug
|
||||||
|
- `tag` - Filter by tag
|
||||||
|
- `status` - Filter by status (draft/published/archived)
|
||||||
|
- `search` - Full-text search in title and content
|
||||||
|
- `page` - Page number (default: 1)
|
||||||
|
- `limit` - Items per page (default: 10)
|
||||||
+188
@@ -0,0 +1,188 @@
|
|||||||
|
# Shadcn/ui Setup Complete ✅
|
||||||
|
|
||||||
|
## What's Been Added
|
||||||
|
|
||||||
|
### 1. Core Dependencies Installed
|
||||||
|
- `tailwindcss-animate` - Animations for shadcn/ui
|
||||||
|
- `class-variance-authority` - Variant management
|
||||||
|
- `clsx` - Conditional class names
|
||||||
|
- `tailwind-merge` - Tailwind class merging
|
||||||
|
- `lucide-react` - Icons
|
||||||
|
- `@radix-ui/react-slot` - Headless UI primitives
|
||||||
|
|
||||||
|
### 2. Configuration Files Created
|
||||||
|
- `components.json` - shadcn/ui configuration
|
||||||
|
- `tailwind.config.js` - Tailwind CSS config with shadcn theme
|
||||||
|
- `tsconfig.json` - TypeScript path aliases (`@/`)
|
||||||
|
- `vite.config.ts` - Vite path resolution
|
||||||
|
|
||||||
|
### 3. CSS Setup
|
||||||
|
- Updated `src/index.css` with shadcn/ui:
|
||||||
|
- CSS variables for theming
|
||||||
|
- Dark mode support
|
||||||
|
- Tailwind directives
|
||||||
|
|
||||||
|
### 4. Utilities
|
||||||
|
- `src/lib/utils.ts` - `cn()` function for merging classes
|
||||||
|
|
||||||
|
### 5. UI Components
|
||||||
|
- `src/components/ui/card.tsx` - Card component with:
|
||||||
|
- Card
|
||||||
|
- CardHeader
|
||||||
|
- CardTitle
|
||||||
|
- CardDescription
|
||||||
|
- CardContent
|
||||||
|
- CardFooter
|
||||||
|
|
||||||
|
- `src/components/ui/button.tsx` - Button component with variants:
|
||||||
|
- Default, destructive, outline, secondary, ghost, link
|
||||||
|
- Sizes: default, sm, lg, icon
|
||||||
|
|
||||||
|
### 6. Home Page
|
||||||
|
- `src/routes/__root.tsx` - Article grid with:
|
||||||
|
- Cards displaying articles
|
||||||
|
- Loading and error states
|
||||||
|
- Responsive grid layout (1/2/3 columns)
|
||||||
|
- Empty state for no articles
|
||||||
|
|
||||||
|
### 7. Routing
|
||||||
|
- Simplified to single route (home page)
|
||||||
|
- All articles displayed on home page as cards
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
### Adding New UI Components
|
||||||
|
|
||||||
|
Use the shadcn/ui CLI to add components:
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest add [component-name]
|
||||||
|
```
|
||||||
|
|
||||||
|
For example:
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest add badge
|
||||||
|
npx shadcn@latest add input
|
||||||
|
npx shadcn@latest add dialog
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Components
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Button } from "@/components/ui/button"
|
||||||
|
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card"
|
||||||
|
|
||||||
|
export function MyComponent() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Title</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p>Content goes here</p>
|
||||||
|
</CardContent>
|
||||||
|
<Button>Click me</Button>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Utility Function
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
export function MyComponent() {
|
||||||
|
return (
|
||||||
|
<div className={cn(
|
||||||
|
"base-class",
|
||||||
|
"conditional-class" && someCondition && "active-class"
|
||||||
|
)}>
|
||||||
|
Content
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Theme Colors
|
||||||
|
Edit `src/index.css` to change color scheme:
|
||||||
|
```css
|
||||||
|
--primary: 222.2 47.4% 11.2%; /* Change primary color */
|
||||||
|
--background: 0 0% 100%; /* Change background */
|
||||||
|
--foreground: 222.2 84% 4.9%; /* Change text color */
|
||||||
|
```
|
||||||
|
|
||||||
|
### Tailwind Config
|
||||||
|
Edit `tailwind.config.js` to:
|
||||||
|
- Add custom colors
|
||||||
|
- Add custom spacing
|
||||||
|
- Add custom breakpoints
|
||||||
|
- Add custom animations
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Add more shadcn/ui components**:
|
||||||
|
- Badge for article tags
|
||||||
|
- Input for search
|
||||||
|
- Dialog for article details
|
||||||
|
- Skeleton for loading states
|
||||||
|
|
||||||
|
2. **Add article detail page**:
|
||||||
|
- Click on article to view full content
|
||||||
|
- Add back button
|
||||||
|
- Add related articles
|
||||||
|
|
||||||
|
3. **Add filters**:
|
||||||
|
- Category filter
|
||||||
|
- Tag filter
|
||||||
|
- Date range filter
|
||||||
|
|
||||||
|
4. **Add dark mode toggle**:
|
||||||
|
- Add button to toggle dark mode
|
||||||
|
- Persist preference in localStorage
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Styles Not Applying
|
||||||
|
1. Check Tailwind is running (look for CSS in browser DevTools)
|
||||||
|
2. Restart dev server
|
||||||
|
3. Check `vite.config.ts` has correct path aliases
|
||||||
|
|
||||||
|
### Imports Not Working
|
||||||
|
1. Verify `tsconfig.json` has `@/` paths configured
|
||||||
|
2. Verify `vite.config.ts` has path resolution
|
||||||
|
3. Restart TypeScript server (if using VSCode)
|
||||||
|
|
||||||
|
### Component Not Found
|
||||||
|
1. Check file path: `src/components/ui/[component].tsx`
|
||||||
|
2. Verify export name: `export const ComponentName = ...`
|
||||||
|
3. Check import path: `@/components/ui/component`
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ └── ui/ # shadcn/ui components
|
||||||
|
│ │ ├── button.tsx
|
||||||
|
│ │ └── card.tsx
|
||||||
|
│ ├── lib/
|
||||||
|
│ │ ├── api.ts # API functions
|
||||||
|
│ │ ├── query-client.ts
|
||||||
|
│ │ └── utils.ts # cn() utility
|
||||||
|
│ ├── routes/
|
||||||
|
│ │ └── __root.tsx # Home page with articles
|
||||||
|
│ ├── index.css # Tailwind + shadcn styles
|
||||||
|
│ └── ...
|
||||||
|
├── components.json # shadcn configuration
|
||||||
|
└── tailwind.config.js # Tailwind configuration
|
||||||
|
```
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [shadcn/ui Documentation](https://ui.shadcn.com/)
|
||||||
|
- [Tailwind CSS](https://tailwindcss.com/)
|
||||||
|
- [Radix UI](https://www.radix-ui.com/)
|
||||||
|
- [Lucide Icons](https://lucide.dev/)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
# Strapi Integration Guide
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
1. Start Strapi CMS:
|
||||||
|
```bash
|
||||||
|
cd cms/cms
|
||||||
|
npm run develop
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Configure environment variables:
|
||||||
|
- In `backend/.env`, add your Strapi API token (create one in Strapi admin panel)
|
||||||
|
|
||||||
|
## Content Types
|
||||||
|
|
||||||
|
Create these content types in Strapi admin panel:
|
||||||
|
|
||||||
|
### Article
|
||||||
|
- Fields:
|
||||||
|
- `title` (Text, required)
|
||||||
|
- `description` (Text)
|
||||||
|
- `content` (Rich Text, required)
|
||||||
|
- `slug` (UID, target field: title)
|
||||||
|
- `publishedAt` (DateTime)
|
||||||
|
- `author` (Relation with Author)
|
||||||
|
- `category` (Relation with Category)
|
||||||
|
- `tags` (Relation with Tag)
|
||||||
|
- Settings: Draft & publish enabled
|
||||||
|
|
||||||
|
### Author
|
||||||
|
- Fields:
|
||||||
|
- `name` (Text, required)
|
||||||
|
- `bio` (Text)
|
||||||
|
- `avatar` (Media)
|
||||||
|
|
||||||
|
### Category
|
||||||
|
- Fields:
|
||||||
|
- `name` (Text, required)
|
||||||
|
- `description` (Text)
|
||||||
|
- `parent` (Relation with Category, self-referential)
|
||||||
|
|
||||||
|
### Tag
|
||||||
|
- Fields:
|
||||||
|
- `name` (Text, required)
|
||||||
|
|
||||||
|
## Webhook Configuration
|
||||||
|
|
||||||
|
In Strapi admin panel, go to Settings → Webhooks and create a webhook:
|
||||||
|
|
||||||
|
### Article Sync Webhook
|
||||||
|
- **Name**: Backend Article Sync
|
||||||
|
- **URL**: `http://localhost:3000/api/v1/webhooks/strapi/article`
|
||||||
|
- **Events**:
|
||||||
|
- ✓ entry.create
|
||||||
|
- ✓ entry.update
|
||||||
|
- ✓ entry.delete
|
||||||
|
- **Headers**: None required for now
|
||||||
|
- **Send headers**: No
|
||||||
|
|
||||||
|
### API Token Creation
|
||||||
|
1. Go to Settings → API Tokens → Create new API Token
|
||||||
|
2. Name: Backend Integration
|
||||||
|
3. Duration: Unlimited
|
||||||
|
4. Token Type: Full access (or customize as needed)
|
||||||
|
5. Copy the generated token to `backend/.env` as `STRAPI_API_TOKEN`
|
||||||
|
|
||||||
|
## Manual Sync
|
||||||
|
|
||||||
|
Trigger a full sync manually:
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3000/api/v1/webhooks/strapi/sync/all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Flow
|
||||||
|
|
||||||
|
1. Content is created/updated in Strapi
|
||||||
|
2. Strapi sends webhook to backend
|
||||||
|
3. Backend fetches the updated content from Strapi API
|
||||||
|
4. Backend syncs content to local SQLite database
|
||||||
|
5. Frontend queries backend API for public content
|
||||||
|
|
||||||
|
## Testing the Integration
|
||||||
|
|
||||||
|
1. Create an article in Strapi
|
||||||
|
2. Publish it
|
||||||
|
3. Check if it appears in backend: `curl http://localhost:3000/api/v1/articles`
|
||||||
|
4. Frontend should now display the article
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Webhook not triggering
|
||||||
|
- Check Strapi webhook logs in admin panel
|
||||||
|
- Verify backend is running on the correct URL
|
||||||
|
- Check network connectivity
|
||||||
|
|
||||||
|
### Sync not working
|
||||||
|
- Verify STRAPI_API_TOKEN is correct
|
||||||
|
- Check Strapi content type permissions
|
||||||
|
- Review backend logs for errors
|
||||||
|
- Test Strapi API directly: `curl http://localhost:1337/api/articles`
|
||||||
|
|
||||||
|
### CORS errors
|
||||||
|
- Ensure backend CORS includes Strapi URL
|
||||||
|
- Update `backend/src/main.ts` CORS configuration
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
# Placebo.mk - Project Structure
|
||||||
|
|
||||||
|
## Monorepo Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
placeboMk/
|
||||||
|
├── backend/ # NestJS API server
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── modules/ # Feature modules
|
||||||
|
│ │ ├── main.ts # Application entry point
|
||||||
|
│ │ └── app.module.ts # Root module
|
||||||
|
│ ├── .env.example
|
||||||
|
│ └── package.json
|
||||||
|
├── frontend/ # TanStack React application
|
||||||
|
│ ├── src/
|
||||||
|
│ │ ├── routes/ # TanStack Router routes
|
||||||
|
│ │ ├── components/ # React components
|
||||||
|
│ │ ├── queries/ # TanStack Query hooks
|
||||||
|
│ │ ├── lib/ # Utilities and API client
|
||||||
|
│ │ ├── types/ # TypeScript types
|
||||||
|
│ │ └── main.tsx # Application entry point
|
||||||
|
│ ├── .env.example
|
||||||
|
│ └── package.json
|
||||||
|
├── cms/ # Strapi CMS (to be initialized)
|
||||||
|
└── AGENTS.md # Agent guidelines
|
||||||
|
```
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
### Backend (NestJS)
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
npm install
|
||||||
|
cp .env.example .env
|
||||||
|
npm run start:dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Frontend (TanStack)
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
cp .env.example .env
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### CMS (Strapi)
|
||||||
|
To be initialized with `npm create strapi-app@latest cms`
|
||||||
|
|
||||||
|
## API Endpoints
|
||||||
|
|
||||||
|
- `GET /api/v1/articles` - List articles with filters
|
||||||
|
- `GET /api/v1/articles/:id` - Get article by ID
|
||||||
|
- `GET /api/v1/articles/slug/:slug` - Get article by slug
|
||||||
|
- `POST /api/v1/articles` - Create article
|
||||||
|
- `PUT /api/v1/articles/:id` - Update article
|
||||||
|
- `DELETE /api/v1/articles/:id` - Delete article
|
||||||
|
|
||||||
|
## Database
|
||||||
|
|
||||||
|
SQLite database with entities:
|
||||||
|
- Articles (title, content, status, tags, etc.)
|
||||||
|
- Authors (name, bio, avatar, etc.)
|
||||||
|
- Categories (name, description, parent-child relations)
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
- **Frontend**: TanStack (React, Query, Router) + Vite
|
||||||
|
- **Backend**: NestJS + TypeORM + SQLite
|
||||||
|
- **CMS**: Strapi
|
||||||
|
- **Language**: TypeScript
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
# Tailwind 4 + Shadcn/ui Setup Complete ✅
|
||||||
|
|
||||||
|
## What's Been Configured
|
||||||
|
|
||||||
|
### 1. Tailwind CSS 4 (CSS-First Approach)
|
||||||
|
- Using `@import` directive (no PostCSS required)
|
||||||
|
- Clean CSS variables for theming
|
||||||
|
- Dark mode support built-in
|
||||||
|
- Responsive utilities
|
||||||
|
|
||||||
|
### 2. Shadcn/ui Components
|
||||||
|
- **Card**: Display article content
|
||||||
|
- CardHeader
|
||||||
|
- CardTitle
|
||||||
|
- CardDescription
|
||||||
|
- CardContent
|
||||||
|
- CardFooter
|
||||||
|
- **Button**: Interactive elements (available but not used yet)
|
||||||
|
|
||||||
|
### 3. Utilities
|
||||||
|
- `cn()` function for merging Tailwind classes
|
||||||
|
- Uses `clsx` for conditional classes
|
||||||
|
- Uses `tailwind-merge` for Tailwind-specific merging
|
||||||
|
|
||||||
|
### 4. TypeScript Configuration
|
||||||
|
- Path aliases configured: `@/` → `./src/`
|
||||||
|
- shadcn/ui aliases: `@/components`, `@/utils`
|
||||||
|
|
||||||
|
## How to Use
|
||||||
|
|
||||||
|
### Using Existing Components
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@/components/ui/card'
|
||||||
|
|
||||||
|
export function MyComponent() {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Title</CardTitle>
|
||||||
|
<CardDescription>Description</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<p>Content goes here</p>
|
||||||
|
</CardContent>
|
||||||
|
<CardFooter>
|
||||||
|
<span>Footer content</span>
|
||||||
|
</CardFooter>
|
||||||
|
</Card>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using Tailwind Classes
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
export function MyComponent() {
|
||||||
|
return (
|
||||||
|
<div className="p-4 bg-background text-foreground">
|
||||||
|
<h1 className="text-2xl font-bold">Title</h1>
|
||||||
|
<p className="text-muted-foreground">Description</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Using the cn() Utility
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
export function MyComponent({ className, isActive }: Props) {
|
||||||
|
return (
|
||||||
|
<div className={cn(
|
||||||
|
"base-class p-4",
|
||||||
|
isActive && "active-class",
|
||||||
|
className
|
||||||
|
)}>
|
||||||
|
Content
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding More Shadcn/ui Components
|
||||||
|
|
||||||
|
### Using the CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npx shadcn@latest add [component-name]
|
||||||
|
```
|
||||||
|
|
||||||
|
### Recommended Components to Add
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx shadcn@latest add badge # For article tags
|
||||||
|
npx shadcn@latest add input # For search
|
||||||
|
npx shadcn@latest add dialog # For article details modal
|
||||||
|
npx shadcn@latest add tabs # For organizing content
|
||||||
|
npx shadcn@latest add separator # For visual separation
|
||||||
|
npx shadcn@latest add skeleton # For loading states
|
||||||
|
npx shadcn@latest add pagination # For article list navigation
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Installation
|
||||||
|
|
||||||
|
If CLI doesn't work, you can manually create components:
|
||||||
|
|
||||||
|
1. Visit [ui.shadcn.com](https://ui.shadcn.com)
|
||||||
|
2. Find the component you want
|
||||||
|
3. Copy the code
|
||||||
|
4. Create file: `frontend/src/components/ui/[component].tsx`
|
||||||
|
5. Import and use
|
||||||
|
|
||||||
|
## Customization
|
||||||
|
|
||||||
|
### Theme Colors
|
||||||
|
|
||||||
|
Edit `src/index.css`:
|
||||||
|
|
||||||
|
```css
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%; /* Background color */
|
||||||
|
--foreground: 222.2 84% 4.9%; /* Text color */
|
||||||
|
--primary: 222.2 47.4% 11.2%; /* Primary color */
|
||||||
|
--card: 0 0% 100%; /* Card background */
|
||||||
|
/* ... more variables */
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Adding Custom Colors to Tailwind
|
||||||
|
|
||||||
|
Edit `tailwind.config.js`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
brand: {
|
||||||
|
50: '#f0f9ff',
|
||||||
|
100: '#e0e7ff',
|
||||||
|
500: '#3b82f6',
|
||||||
|
// ... more shades
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Current Implementation
|
||||||
|
|
||||||
|
### App.tsx
|
||||||
|
|
||||||
|
The main app displays:
|
||||||
|
- Header with "Placebo.mk" title
|
||||||
|
- Subtitle "Sarcastic news from Macedonia"
|
||||||
|
- Responsive grid of article cards (1/2/3 columns)
|
||||||
|
- Each card shows:
|
||||||
|
- Featured image (if available)
|
||||||
|
- Title (truncated to 2 lines)
|
||||||
|
- Excerpt (truncated to 3 lines)
|
||||||
|
- Content preview (truncated to 4 lines)
|
||||||
|
- Creation date
|
||||||
|
- View count
|
||||||
|
- Loading and error states
|
||||||
|
- Empty state when no articles
|
||||||
|
|
||||||
|
## Dark Mode
|
||||||
|
|
||||||
|
To enable dark mode, add a toggle:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
|
export function ThemeToggle() {
|
||||||
|
useEffect(() => {
|
||||||
|
const isDark = localStorage.getItem('theme') === 'dark'
|
||||||
|
document.documentElement.classList.toggle('dark', isDark)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const toggleTheme = () => {
|
||||||
|
const isDark = document.documentElement.classList.toggle('dark')
|
||||||
|
localStorage.setItem('theme', isDark ? 'dark' : 'light')
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button onClick={toggleTheme}>
|
||||||
|
Toggle Theme
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
### Tailwind Classes Not Applying
|
||||||
|
|
||||||
|
1. Check that `@tailwind` directives are in `src/index.css`
|
||||||
|
2. Restart dev server: `npm run dev`
|
||||||
|
3. Hard refresh browser: Ctrl+Shift+R
|
||||||
|
4. Check browser DevTools → Elements → Computed Styles
|
||||||
|
|
||||||
|
### Component Not Found
|
||||||
|
|
||||||
|
1. Check file exists: `frontend/src/components/ui/[component].tsx`
|
||||||
|
2. Check export name matches: `export const ComponentName = ...`
|
||||||
|
3. Check import path: `@/components/ui/component`
|
||||||
|
4. Restart TypeScript server (VSCode: Cmd+Shift+P → "TypeScript: Restart TS Server")
|
||||||
|
|
||||||
|
### TypeScript Errors
|
||||||
|
|
||||||
|
1. Check `tsconfig.json` has `@/` paths configured
|
||||||
|
2. Restart TypeScript server
|
||||||
|
3. Run: `npm run type-check`
|
||||||
|
4. Check `components.json` aliases
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
1. **Image Optimization**: Add `loading="lazy"` to article images
|
||||||
|
2. **Code Splitting**: Use `lazy()` for large components
|
||||||
|
3. **Query Caching**: TanStack Query caches automatically
|
||||||
|
4. **Bundle Analysis**: Run `npm run build` and check bundle size
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
frontend/
|
||||||
|
├── src/
|
||||||
|
│ ├── components/
|
||||||
|
│ │ └── ui/ # shadcn/ui components
|
||||||
|
│ │ ├── button.tsx
|
||||||
|
│ │ └── card.tsx
|
||||||
|
│ ├── lib/
|
||||||
|
│ │ ├── api.ts # API functions
|
||||||
|
│ │ ├── query-client.ts # TanStack Query
|
||||||
|
│ │ └── utils.ts # cn() utility
|
||||||
|
│ ├── App.tsx # Main application
|
||||||
|
│ ├── index.css # Tailwind + CSS variables
|
||||||
|
│ └── main.tsx
|
||||||
|
├── components.json # shadcn config
|
||||||
|
├── tailwind.config.js # Tailwind configuration
|
||||||
|
└── package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
1. **Add article detail page**: Click on card to view full article
|
||||||
|
2. **Add search input**: Filter articles by title/content
|
||||||
|
3. **Add category filter**: Filter by article category
|
||||||
|
4. **Add tag badges**: Display article tags
|
||||||
|
5. **Add pagination**: Navigate through large article lists
|
||||||
|
6. **Add dark mode toggle**: Switch between light/dark themes
|
||||||
|
7. **Add article detail modal**: View article without leaving page
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
- [Tailwind CSS 4 Docs](https://tailwindcss.com/docs/installation/using-postcss)
|
||||||
|
- [Shadcn/ui Documentation](https://ui.shadcn.com)
|
||||||
|
- [Radix UI](https://www.radix-ui.com/) - Underlying component library
|
||||||
|
- [Lucide Icons](https://lucide.dev/) - Icon library (installed)
|
||||||
|
- [TanStack Query Docs](https://tanstack.com/query/latest)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
PORT=3000
|
||||||
|
DATABASE_PATH=./database.sqlite
|
||||||
|
NODE_ENV=development
|
||||||
|
FRONTEND_URL=http://localhost:5173
|
||||||
|
|
||||||
|
STRAPI_URL=http://localhost:1337
|
||||||
|
STRAPI_API_TOKEN=your-strapi-api-token-here
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "all"
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="120" alt="Nest Logo" /></a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
|
||||||
|
[circleci-url]: https://circleci.com/gh/nestjs/nest
|
||||||
|
|
||||||
|
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
|
||||||
|
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
|
||||||
|
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
|
||||||
|
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
|
||||||
|
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg" alt="Donate us"/></a>
|
||||||
|
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
|
||||||
|
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow" alt="Follow us on Twitter"></a>
|
||||||
|
</p>
|
||||||
|
<!--[](https://opencollective.com/nest#backer)
|
||||||
|
[](https://opencollective.com/nest#sponsor)-->
|
||||||
|
|
||||||
|
## Description
|
||||||
|
|
||||||
|
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
## Compile and run the project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# development
|
||||||
|
$ npm run start
|
||||||
|
|
||||||
|
# watch mode
|
||||||
|
$ npm run start:dev
|
||||||
|
|
||||||
|
# production mode
|
||||||
|
$ npm run start:prod
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# unit tests
|
||||||
|
$ npm run test
|
||||||
|
|
||||||
|
# e2e tests
|
||||||
|
$ npm run test:e2e
|
||||||
|
|
||||||
|
# test coverage
|
||||||
|
$ npm run test:cov
|
||||||
|
```
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
|
||||||
|
When you're ready to deploy your NestJS application to production, there are some key steps you can take to ensure it runs as efficiently as possible. Check out the [deployment documentation](https://docs.nestjs.com/deployment) for more information.
|
||||||
|
|
||||||
|
If you are looking for a cloud-based platform to deploy your NestJS application, check out [Mau](https://mau.nestjs.com), our official platform for deploying NestJS applications on AWS. Mau makes deployment straightforward and fast, requiring just a few simple steps:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
$ npm install -g @nestjs/mau
|
||||||
|
$ mau deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
With Mau, you can deploy your application in just a few clicks, allowing you to focus on building features rather than managing infrastructure.
|
||||||
|
|
||||||
|
## Resources
|
||||||
|
|
||||||
|
Check out a few resources that may come in handy when working with NestJS:
|
||||||
|
|
||||||
|
- Visit the [NestJS Documentation](https://docs.nestjs.com) to learn more about the framework.
|
||||||
|
- For questions and support, please visit our [Discord channel](https://discord.gg/G7Qnnhy).
|
||||||
|
- To dive deeper and get more hands-on experience, check out our official video [courses](https://courses.nestjs.com/).
|
||||||
|
- Deploy your application to AWS with the help of [NestJS Mau](https://mau.nestjs.com) in just a few clicks.
|
||||||
|
- Visualize your application graph and interact with the NestJS application in real-time using [NestJS Devtools](https://devtools.nestjs.com).
|
||||||
|
- Need help with your project (part-time to full-time)? Check out our official [enterprise support](https://enterprise.nestjs.com).
|
||||||
|
- To stay in the loop and get updates, follow us on [X](https://x.com/nestframework) and [LinkedIn](https://linkedin.com/company/nestjs).
|
||||||
|
- Looking for a job, or have a job to offer? Check out our official [Jobs board](https://jobs.nestjs.com).
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
|
||||||
|
|
||||||
|
## Stay in touch
|
||||||
|
|
||||||
|
- Author - [Kamil Myśliwiec](https://twitter.com/kammysliwiec)
|
||||||
|
- Website - [https://nestjs.com](https://nestjs.com/)
|
||||||
|
- Twitter - [@nestframework](https://twitter.com/nestframework)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Nest is [MIT licensed](https://github.com/nestjs/nest/blob/master/LICENSE).
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
// @ts-check
|
||||||
|
import eslint from '@eslint/js';
|
||||||
|
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended';
|
||||||
|
import globals from 'globals';
|
||||||
|
import tseslint from 'typescript-eslint';
|
||||||
|
|
||||||
|
export default tseslint.config(
|
||||||
|
{
|
||||||
|
ignores: ['eslint.config.mjs'],
|
||||||
|
},
|
||||||
|
eslint.configs.recommended,
|
||||||
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
|
eslintPluginPrettierRecommended,
|
||||||
|
{
|
||||||
|
languageOptions: {
|
||||||
|
globals: {
|
||||||
|
...globals.node,
|
||||||
|
...globals.jest,
|
||||||
|
},
|
||||||
|
sourceType: 'commonjs',
|
||||||
|
parserOptions: {
|
||||||
|
projectService: true,
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
'@typescript-eslint/no-explicit-any': 'off',
|
||||||
|
'@typescript-eslint/no-floating-promises': 'warn',
|
||||||
|
'@typescript-eslint/no-unsafe-argument': 'warn',
|
||||||
|
"prettier/prettier": ["error", { endOfLine: "auto" }],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
Generated
+11942
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,80 @@
|
|||||||
|
{
|
||||||
|
"name": "backend",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "",
|
||||||
|
"author": "",
|
||||||
|
"private": true,
|
||||||
|
"license": "UNLICENSED",
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
|
||||||
|
"start": "nest start",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"start:debug": "nest start --debug --watch",
|
||||||
|
"start:prod": "node dist/main",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"lint:fix": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config ./test/jest-e2e.json"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/axios": "^4.0.1",
|
||||||
|
"@nestjs/common": "^11.0.1",
|
||||||
|
"@nestjs/config": "^4.0.2",
|
||||||
|
"@nestjs/core": "^11.0.1",
|
||||||
|
"@nestjs/platform-express": "^11.0.1",
|
||||||
|
"@nestjs/typeorm": "^11.0.0",
|
||||||
|
"class-transformer": "^0.5.1",
|
||||||
|
"class-validator": "^0.14.3",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1",
|
||||||
|
"sqlite3": "^5.1.7",
|
||||||
|
"typeorm": "^0.3.28"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
"@eslint/js": "^9.18.0",
|
||||||
|
"@nestjs/cli": "^11.0.0",
|
||||||
|
"@nestjs/schematics": "^11.0.0",
|
||||||
|
"@nestjs/testing": "^11.0.1",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
|
"@types/node": "^22.10.7",
|
||||||
|
"@types/supertest": "^6.0.2",
|
||||||
|
"eslint": "^9.18.0",
|
||||||
|
"eslint-config-prettier": "^10.0.1",
|
||||||
|
"eslint-plugin-prettier": "^5.2.2",
|
||||||
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^30.0.0",
|
||||||
|
"prettier": "^3.4.2",
|
||||||
|
"source-map-support": "^0.5.21",
|
||||||
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.2.5",
|
||||||
|
"ts-loader": "^9.5.2",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"tsconfig-paths": "^4.2.0",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"typescript-eslint": "^8.20.0"
|
||||||
|
},
|
||||||
|
"jest": {
|
||||||
|
"moduleFileExtensions": [
|
||||||
|
"js",
|
||||||
|
"json",
|
||||||
|
"ts"
|
||||||
|
],
|
||||||
|
"rootDir": "src",
|
||||||
|
"testRegex": ".*\\.spec\\.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
},
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"**/*.(t|j)s"
|
||||||
|
],
|
||||||
|
"coverageDirectory": "../coverage",
|
||||||
|
"testEnvironment": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
describe('AppController', () => {
|
||||||
|
let appController: AppController;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const app: TestingModule = await Test.createTestingModule({
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
appController = app.get<AppController>(AppController);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('root', () => {
|
||||||
|
it('should return "Hello World!"', () => {
|
||||||
|
expect(appController.getHello()).toBe('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { Controller, Get } from '@nestjs/common';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class AppController {
|
||||||
|
constructor(private readonly appService: AppService) {}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
getHello(): string {
|
||||||
|
return this.appService.getHello();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ConfigModule } from '@nestjs/config';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { AppController } from './app.controller';
|
||||||
|
import { AppService } from './app.service';
|
||||||
|
import { ArticlesModule } from './modules/articles.module';
|
||||||
|
import { StrapiModule } from './modules/strapi.module';
|
||||||
|
import { Article, Author, Category } from './modules/entities';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ConfigModule.forRoot({
|
||||||
|
isGlobal: true,
|
||||||
|
}),
|
||||||
|
TypeOrmModule.forRoot({
|
||||||
|
type: 'sqlite',
|
||||||
|
database: process.env.DATABASE_PATH ?? './database.sqlite',
|
||||||
|
entities: [Article, Author, Category],
|
||||||
|
synchronize: process.env.NODE_ENV !== 'production',
|
||||||
|
logging: process.env.NODE_ENV === 'development',
|
||||||
|
}),
|
||||||
|
ArticlesModule,
|
||||||
|
StrapiModule,
|
||||||
|
],
|
||||||
|
controllers: [AppController],
|
||||||
|
providers: [AppService],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class AppService {
|
||||||
|
getHello(): string {
|
||||||
|
return 'Hello World!';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
|
||||||
|
const allowedOrigins = [
|
||||||
|
process.env.FRONTEND_URL ?? 'http://localhost:5173',
|
||||||
|
process.env.STRAPI_URL ?? 'http://localhost:1337',
|
||||||
|
];
|
||||||
|
|
||||||
|
app.enableCors({
|
||||||
|
origin: allowedOrigins,
|
||||||
|
credentials: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
app.setGlobalPrefix('api/v1');
|
||||||
|
await app.listen(process.env.PORT ?? 3000);
|
||||||
|
}
|
||||||
|
void bootstrap();
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import {
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Post,
|
||||||
|
Put,
|
||||||
|
Delete,
|
||||||
|
Body,
|
||||||
|
Param,
|
||||||
|
Query,
|
||||||
|
ValidationPipe,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { ArticlesService } from './articles.service';
|
||||||
|
import {
|
||||||
|
CreateArticleDto,
|
||||||
|
UpdateArticleDto,
|
||||||
|
FindArticlesDto,
|
||||||
|
} from './articles.dto';
|
||||||
|
|
||||||
|
@Controller('articles')
|
||||||
|
export class ArticlesController {
|
||||||
|
constructor(private readonly articlesService: ArticlesService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@Body(new ValidationPipe({ transform: true })) dto: CreateArticleDto) {
|
||||||
|
return this.articlesService.create(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get()
|
||||||
|
findAll(
|
||||||
|
@Query(new ValidationPipe({ transform: true })) dto: FindArticlesDto,
|
||||||
|
) {
|
||||||
|
return this.articlesService.findAll(dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':id')
|
||||||
|
findOne(@Param('id') id: string) {
|
||||||
|
return this.articlesService.findOne(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('slug/:slug')
|
||||||
|
findBySlug(@Param('slug') slug: string) {
|
||||||
|
return this.articlesService.findBySlug(slug);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Put(':id')
|
||||||
|
update(
|
||||||
|
@Param('id') id: string,
|
||||||
|
@Body(new ValidationPipe({ transform: true })) dto: UpdateArticleDto,
|
||||||
|
) {
|
||||||
|
return this.articlesService.update(id, dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete(':id')
|
||||||
|
remove(@Param('id') id: string) {
|
||||||
|
return this.articlesService.remove(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
import {
|
||||||
|
IsString,
|
||||||
|
IsOptional,
|
||||||
|
IsEnum,
|
||||||
|
IsArray,
|
||||||
|
IsUUID,
|
||||||
|
IsNumber,
|
||||||
|
} from 'class-validator';
|
||||||
|
import { ArticleStatus } from './entities';
|
||||||
|
|
||||||
|
export class CreateArticleDto {
|
||||||
|
@IsString()
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@IsString()
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
excerpt?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
slug?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
featuredImage?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
tags?: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(ArticleStatus)
|
||||||
|
status?: ArticleStatus;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
strapiId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
authorId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
categoryId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UpdateArticleDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
content?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
excerpt?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
slug?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
featuredImage?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsArray()
|
||||||
|
@IsString({ each: true })
|
||||||
|
tags?: string[];
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(ArticleStatus)
|
||||||
|
status?: ArticleStatus;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
strapiId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
authorId?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsUUID()
|
||||||
|
categoryId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FindArticlesDto {
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
category?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
author?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
tag?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(ArticleStatus)
|
||||||
|
status?: ArticleStatus;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsString()
|
||||||
|
search?: string;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
page?: number;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsNumber()
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { ArticlesService } from './articles.service';
|
||||||
|
import { ArticlesController } from './articles.controller';
|
||||||
|
import { Article, Author, Category } from './entities';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([Article, Author, Category])],
|
||||||
|
controllers: [ArticlesController],
|
||||||
|
providers: [ArticlesService],
|
||||||
|
exports: [ArticlesService],
|
||||||
|
})
|
||||||
|
export class ArticlesModule {}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { Article, ArticleStatus } from './entities';
|
||||||
|
import {
|
||||||
|
CreateArticleDto,
|
||||||
|
UpdateArticleDto,
|
||||||
|
FindArticlesDto,
|
||||||
|
} from './articles.dto';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ArticlesService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(Article)
|
||||||
|
private readonly articleRepository: Repository<Article>,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(dto: CreateArticleDto): Promise<Article> {
|
||||||
|
const article = this.articleRepository.create({
|
||||||
|
...dto,
|
||||||
|
status: dto.status || ArticleStatus.DRAFT,
|
||||||
|
});
|
||||||
|
return await this.articleRepository.save(article);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findAll(
|
||||||
|
dto: FindArticlesDto,
|
||||||
|
): Promise<{ data: Article[]; total: number }> {
|
||||||
|
const {
|
||||||
|
category,
|
||||||
|
author,
|
||||||
|
tag,
|
||||||
|
status = ArticleStatus.PUBLISHED,
|
||||||
|
search,
|
||||||
|
page = 1,
|
||||||
|
limit = 10,
|
||||||
|
} = dto;
|
||||||
|
|
||||||
|
const queryBuilder = this.articleRepository
|
||||||
|
.createQueryBuilder('article')
|
||||||
|
.leftJoinAndSelect('article.author', 'author')
|
||||||
|
.leftJoinAndSelect('article.category', 'category')
|
||||||
|
.where('article.status = :status', { status });
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
queryBuilder.andWhere('category.slug = :category', { category });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (author) {
|
||||||
|
queryBuilder.andWhere('author.slug = :author', { author });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tag) {
|
||||||
|
queryBuilder.andWhere(':tag = ANY(article.tags)', { tag });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (search) {
|
||||||
|
queryBuilder.andWhere(
|
||||||
|
'(article.title ILIKE :search OR article.content ILIKE :search)',
|
||||||
|
{ search: `%${search}%` },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [data, total] = await queryBuilder
|
||||||
|
.orderBy('article.createdAt', 'DESC')
|
||||||
|
.skip((page - 1) * limit)
|
||||||
|
.take(limit)
|
||||||
|
.getManyAndCount();
|
||||||
|
|
||||||
|
return { data, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
async findOne(id: string): Promise<Article> {
|
||||||
|
const article = await this.articleRepository.findOne({
|
||||||
|
where: { id },
|
||||||
|
relations: ['author', 'category'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!article) {
|
||||||
|
throw new NotFoundException(`Article with ID ${id} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return article;
|
||||||
|
}
|
||||||
|
|
||||||
|
async findBySlug(slug: string): Promise<Article> {
|
||||||
|
const article = await this.articleRepository.findOne({
|
||||||
|
where: { slug },
|
||||||
|
relations: ['author', 'category'],
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!article) {
|
||||||
|
throw new NotFoundException(`Article with slug ${slug} not found`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return article;
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(id: string, dto: UpdateArticleDto): Promise<Article> {
|
||||||
|
const article = await this.findOne(id);
|
||||||
|
Object.assign(article, dto);
|
||||||
|
return await this.articleRepository.save(article);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(id: string): Promise<void> {
|
||||||
|
const article = await this.findOne(id);
|
||||||
|
await this.articleRepository.remove(article);
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncFromStrapi(
|
||||||
|
strapiId: string,
|
||||||
|
data: Partial<CreateArticleDto>,
|
||||||
|
): Promise<Article> {
|
||||||
|
let article = await this.articleRepository.findOne({ where: { strapiId } });
|
||||||
|
|
||||||
|
if (!article) {
|
||||||
|
article = this.articleRepository.create({
|
||||||
|
strapiId,
|
||||||
|
...data,
|
||||||
|
status: data.status || ArticleStatus.DRAFT,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Object.assign(article, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.articleRepository.save(article);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,129 @@
|
|||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
PrimaryGeneratedColumn,
|
||||||
|
Column,
|
||||||
|
CreateDateColumn,
|
||||||
|
UpdateDateColumn,
|
||||||
|
ManyToOne,
|
||||||
|
JoinColumn,
|
||||||
|
} from 'typeorm';
|
||||||
|
|
||||||
|
export enum ArticleStatus {
|
||||||
|
DRAFT = 'draft',
|
||||||
|
PUBLISHED = 'published',
|
||||||
|
ARCHIVED = 'archived',
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('authors')
|
||||||
|
export class Author {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ unique: true })
|
||||||
|
slug: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
bio: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
avatar: string;
|
||||||
|
|
||||||
|
@Column({ default: false })
|
||||||
|
isActive: boolean;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('categories')
|
||||||
|
export class Category {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
name: string;
|
||||||
|
|
||||||
|
@Column({ unique: true })
|
||||||
|
slug: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
description: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
parentId: string;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
order: number;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@ManyToOne(() => Category, { onDelete: 'SET NULL' })
|
||||||
|
@JoinColumn({ name: 'parentId' })
|
||||||
|
parent: Category;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity('articles')
|
||||||
|
export class Article {
|
||||||
|
@PrimaryGeneratedColumn('uuid')
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
title: string;
|
||||||
|
|
||||||
|
@Column('text')
|
||||||
|
content: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', nullable: true })
|
||||||
|
excerpt: string;
|
||||||
|
|
||||||
|
@Column({ default: '' })
|
||||||
|
slug: string;
|
||||||
|
|
||||||
|
@Column({ default: '' })
|
||||||
|
featuredImage: string;
|
||||||
|
|
||||||
|
@Column({ type: 'text', default: '[]' })
|
||||||
|
tags: string[];
|
||||||
|
|
||||||
|
@Column({
|
||||||
|
type: 'text',
|
||||||
|
default: 'draft',
|
||||||
|
})
|
||||||
|
status: ArticleStatus;
|
||||||
|
|
||||||
|
@Column({ default: 0 })
|
||||||
|
views: number;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
strapiId: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
authorId: string;
|
||||||
|
|
||||||
|
@Column({ nullable: true })
|
||||||
|
categoryId: string;
|
||||||
|
|
||||||
|
@CreateDateColumn()
|
||||||
|
createdAt: Date;
|
||||||
|
|
||||||
|
@UpdateDateColumn()
|
||||||
|
updatedAt: Date;
|
||||||
|
|
||||||
|
@ManyToOne(() => Author, { onDelete: 'SET NULL' })
|
||||||
|
@JoinColumn({ name: 'authorId' })
|
||||||
|
author: Author;
|
||||||
|
|
||||||
|
@ManyToOne(() => Category, { onDelete: 'SET NULL' })
|
||||||
|
@JoinColumn({ name: 'categoryId' })
|
||||||
|
category: Category;
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { Controller, Post, Body } from '@nestjs/common';
|
||||||
|
import { StrapiService } from './strapi.service';
|
||||||
|
|
||||||
|
interface WebhookBody {
|
||||||
|
event: 'entry.create' | 'entry.update' | 'entry.delete';
|
||||||
|
model: string;
|
||||||
|
entry: {
|
||||||
|
documentId: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller('webhooks/strapi')
|
||||||
|
export class StrapiController {
|
||||||
|
constructor(private readonly strapiService: StrapiService) {}
|
||||||
|
|
||||||
|
@Post('article')
|
||||||
|
async handleArticleWebhook(@Body() body: WebhookBody) {
|
||||||
|
const { event, model, entry } = body;
|
||||||
|
|
||||||
|
if (model !== 'article') {
|
||||||
|
return { message: 'Ignored: not an article' };
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.strapiService.handleWebhook(event, entry);
|
||||||
|
return { message: 'Webhook processed successfully' };
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('sync/all')
|
||||||
|
async syncAllArticles() {
|
||||||
|
await this.strapiService.syncArticles();
|
||||||
|
return { message: 'Sync completed' };
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { HttpModule } from '@nestjs/axios';
|
||||||
|
import { StrapiService } from './strapi.service';
|
||||||
|
import { StrapiController } from './strapi.controller';
|
||||||
|
import { ArticlesModule } from './articles.module';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [HttpModule, ArticlesModule],
|
||||||
|
controllers: [StrapiController],
|
||||||
|
providers: [StrapiService],
|
||||||
|
exports: [StrapiService],
|
||||||
|
})
|
||||||
|
export class StrapiModule {}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
import { Injectable, Logger } from '@nestjs/common';
|
||||||
|
import { ConfigService } from '@nestjs/config';
|
||||||
|
import { HttpService } from '@nestjs/axios';
|
||||||
|
import { lastValueFrom } from 'rxjs';
|
||||||
|
import { ArticlesService } from './articles.service';
|
||||||
|
import { CreateArticleDto } from './articles.dto';
|
||||||
|
import { ArticleStatus } from './entities';
|
||||||
|
|
||||||
|
interface StrapiArticle {
|
||||||
|
id: number;
|
||||||
|
documentId: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
content: string;
|
||||||
|
slug: string;
|
||||||
|
publishedAt: string | null;
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StrapiResponse<T> {
|
||||||
|
data: T;
|
||||||
|
meta: {
|
||||||
|
pagination: {
|
||||||
|
page: number;
|
||||||
|
pageSize: number;
|
||||||
|
pageCount: number;
|
||||||
|
total: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class StrapiService {
|
||||||
|
private readonly logger = new Logger(StrapiService.name);
|
||||||
|
private readonly strapiUrl: string;
|
||||||
|
private readonly strapiApiToken: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly configService: ConfigService,
|
||||||
|
private readonly httpService: HttpService,
|
||||||
|
private readonly articlesService: ArticlesService,
|
||||||
|
) {
|
||||||
|
this.strapiUrl =
|
||||||
|
this.configService.get<string>('STRAPI_URL') || 'http://localhost:1337';
|
||||||
|
this.strapiApiToken =
|
||||||
|
this.configService.get<string>('STRAPI_API_TOKEN') || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
private getHeaders() {
|
||||||
|
const headers: Record<string, string> = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
if (this.strapiApiToken) {
|
||||||
|
headers['Authorization'] = `Bearer ${this.strapiApiToken}`;
|
||||||
|
}
|
||||||
|
return headers;
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncArticles(): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.logger.log('Starting articles sync from Strapi...');
|
||||||
|
|
||||||
|
const response = await lastValueFrom(
|
||||||
|
this.httpService.get<StrapiResponse<StrapiArticle[]>>(
|
||||||
|
`${this.strapiUrl}/api/articles`,
|
||||||
|
{
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const strapiArticles = response.data.data;
|
||||||
|
let syncedCount = 0;
|
||||||
|
|
||||||
|
for (const strapiArticle of strapiArticles) {
|
||||||
|
const articleData: Partial<CreateArticleDto> = {
|
||||||
|
title: strapiArticle.title,
|
||||||
|
excerpt: strapiArticle.description,
|
||||||
|
content: strapiArticle.content,
|
||||||
|
slug: strapiArticle.slug,
|
||||||
|
status: strapiArticle.publishedAt
|
||||||
|
? ArticleStatus.PUBLISHED
|
||||||
|
: ArticleStatus.DRAFT,
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.articlesService.syncFromStrapi(
|
||||||
|
strapiArticle.documentId,
|
||||||
|
articleData,
|
||||||
|
);
|
||||||
|
syncedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.log(
|
||||||
|
`Successfully synced ${syncedCount} articles from Strapi`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Failed to sync articles from Strapi', error);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async syncSingleArticle(strapiId: string): Promise<void> {
|
||||||
|
try {
|
||||||
|
this.logger.log(`Syncing single article from Strapi: ${strapiId}`);
|
||||||
|
|
||||||
|
const response = await lastValueFrom(
|
||||||
|
this.httpService.get<StrapiResponse<StrapiArticle>>(
|
||||||
|
`${this.strapiUrl}/api/articles/${strapiId}`,
|
||||||
|
{
|
||||||
|
headers: this.getHeaders(),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
const strapiArticle = response.data.data;
|
||||||
|
const articleData: Partial<CreateArticleDto> = {
|
||||||
|
title: strapiArticle.title,
|
||||||
|
excerpt: strapiArticle.description,
|
||||||
|
content: strapiArticle.content,
|
||||||
|
slug: strapiArticle.slug,
|
||||||
|
status: strapiArticle.publishedAt
|
||||||
|
? ArticleStatus.PUBLISHED
|
||||||
|
: ArticleStatus.DRAFT,
|
||||||
|
tags: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
await this.articlesService.syncFromStrapi(
|
||||||
|
strapiArticle.documentId,
|
||||||
|
articleData,
|
||||||
|
);
|
||||||
|
this.logger.log(`Successfully synced article: ${strapiArticle.title}`);
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(
|
||||||
|
`Failed to sync article ${strapiId} from Strapi`,
|
||||||
|
error,
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async handleWebhook(
|
||||||
|
event: 'entry.create' | 'entry.update' | 'entry.delete',
|
||||||
|
data: { documentId: string },
|
||||||
|
): Promise<void> {
|
||||||
|
this.logger.log(`Received webhook event: ${event}`);
|
||||||
|
|
||||||
|
if (event === 'entry.delete') {
|
||||||
|
this.logger.log(`Handling delete for document: ${data.documentId}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.syncSingleArticle(data.documentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { INestApplication } from '@nestjs/common';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { App } from 'supertest/types';
|
||||||
|
import { AppModule } from './../src/app.module';
|
||||||
|
|
||||||
|
describe('AppController (e2e)', () => {
|
||||||
|
let app: INestApplication<App>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const moduleFixture: TestingModule = await Test.createTestingModule({
|
||||||
|
imports: [AppModule],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
app = moduleFixture.createNestApplication();
|
||||||
|
await app.init();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('/ (GET)', () => {
|
||||||
|
return request(app.getHttpServer())
|
||||||
|
.get('/')
|
||||||
|
.expect(200)
|
||||||
|
.expect('Hello World!');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"moduleFileExtensions": ["js", "json", "ts"],
|
||||||
|
"rootDir": ".",
|
||||||
|
"testEnvironment": "node",
|
||||||
|
"testRegex": ".e2e-spec.ts$",
|
||||||
|
"transform": {
|
||||||
|
"^.+\\.(t|j)s$": "ts-jest"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "nodenext",
|
||||||
|
"moduleResolution": "nodenext",
|
||||||
|
"resolvePackageJsonExports": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"declaration": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"target": "ES2023",
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"incremental": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strictNullChecks": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"noImplicitAny": true,
|
||||||
|
"strictBindCallApply": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
HOST=0.0.0.0
|
||||||
|
PORT=1337
|
||||||
|
APP_KEYS="toBeModified1,toBeModified2"
|
||||||
|
API_TOKEN_SALT=tobemodified
|
||||||
|
ADMIN_JWT_SECRET=tobemodified
|
||||||
|
TRANSFER_TOKEN_SALT=tobemodified
|
||||||
|
JWT_SECRET=tobemodified
|
||||||
|
ENCRYPTION_KEY=tobemodified
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
############################
|
||||||
|
# OS X
|
||||||
|
############################
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
.AppleDouble
|
||||||
|
.LSOverride
|
||||||
|
Icon
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
._*
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Linux
|
||||||
|
############################
|
||||||
|
|
||||||
|
*~
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Windows
|
||||||
|
############################
|
||||||
|
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
$RECYCLE.BIN/
|
||||||
|
*.cab
|
||||||
|
*.msi
|
||||||
|
*.msm
|
||||||
|
*.msp
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Packages
|
||||||
|
############################
|
||||||
|
|
||||||
|
*.7z
|
||||||
|
*.csv
|
||||||
|
*.dat
|
||||||
|
*.dmg
|
||||||
|
*.gz
|
||||||
|
*.iso
|
||||||
|
*.jar
|
||||||
|
*.rar
|
||||||
|
*.tar
|
||||||
|
*.zip
|
||||||
|
*.com
|
||||||
|
*.class
|
||||||
|
*.dll
|
||||||
|
*.exe
|
||||||
|
*.o
|
||||||
|
*.seed
|
||||||
|
*.so
|
||||||
|
*.swo
|
||||||
|
*.swp
|
||||||
|
*.swn
|
||||||
|
*.swm
|
||||||
|
*.out
|
||||||
|
*.pid
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Logs and databases
|
||||||
|
############################
|
||||||
|
|
||||||
|
.tmp
|
||||||
|
*.log
|
||||||
|
*.sql
|
||||||
|
*.sqlite
|
||||||
|
*.sqlite3
|
||||||
|
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Misc.
|
||||||
|
############################
|
||||||
|
|
||||||
|
*#
|
||||||
|
ssl
|
||||||
|
.idea
|
||||||
|
nbproject
|
||||||
|
public/uploads/*
|
||||||
|
!public/uploads/.gitkeep
|
||||||
|
.tsbuildinfo
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Node.js
|
||||||
|
############################
|
||||||
|
|
||||||
|
lib-cov
|
||||||
|
lcov.info
|
||||||
|
pids
|
||||||
|
logs
|
||||||
|
results
|
||||||
|
node_modules
|
||||||
|
.node_history
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Package managers
|
||||||
|
############################
|
||||||
|
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/cache
|
||||||
|
!.yarn/unplugged
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/sdks
|
||||||
|
!.yarn/versions
|
||||||
|
.pnp.*
|
||||||
|
yarn-error.log
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Tests
|
||||||
|
############################
|
||||||
|
|
||||||
|
coverage
|
||||||
|
|
||||||
|
############################
|
||||||
|
# Strapi
|
||||||
|
############################
|
||||||
|
|
||||||
|
.env
|
||||||
|
license.txt
|
||||||
|
exports
|
||||||
|
.strapi
|
||||||
|
dist
|
||||||
|
build
|
||||||
|
.strapi-updater.json
|
||||||
|
.strapi-cloud.json
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
# 🚀 Getting started with Strapi
|
||||||
|
|
||||||
|
Strapi comes with a full featured [Command Line Interface](https://docs.strapi.io/dev-docs/cli) (CLI) which lets you scaffold and manage your project in seconds.
|
||||||
|
|
||||||
|
### `develop`
|
||||||
|
|
||||||
|
Start your Strapi application with autoReload enabled. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-develop)
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run develop
|
||||||
|
# or
|
||||||
|
yarn develop
|
||||||
|
```
|
||||||
|
|
||||||
|
### `start`
|
||||||
|
|
||||||
|
Start your Strapi application with autoReload disabled. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-start)
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run start
|
||||||
|
# or
|
||||||
|
yarn start
|
||||||
|
```
|
||||||
|
|
||||||
|
### `build`
|
||||||
|
|
||||||
|
Build your admin panel. [Learn more](https://docs.strapi.io/dev-docs/cli#strapi-build)
|
||||||
|
|
||||||
|
```
|
||||||
|
npm run build
|
||||||
|
# or
|
||||||
|
yarn build
|
||||||
|
```
|
||||||
|
|
||||||
|
## ⚙️ Deployment
|
||||||
|
|
||||||
|
Strapi gives you many possible deployment options for your project including [Strapi Cloud](https://cloud.strapi.io). Browse the [deployment section of the documentation](https://docs.strapi.io/dev-docs/deployment) to find the best solution for your use case.
|
||||||
|
|
||||||
|
```
|
||||||
|
yarn strapi deploy
|
||||||
|
```
|
||||||
|
|
||||||
|
## 📚 Learn more
|
||||||
|
|
||||||
|
- [Resource center](https://strapi.io/resource-center) - Strapi resource center.
|
||||||
|
- [Strapi documentation](https://docs.strapi.io) - Official Strapi documentation.
|
||||||
|
- [Strapi tutorials](https://strapi.io/tutorials) - List of tutorials made by the core team and the community.
|
||||||
|
- [Strapi blog](https://strapi.io/blog) - Official Strapi blog containing articles made by the Strapi team and the community.
|
||||||
|
- [Changelog](https://strapi.io/changelog) - Find out about the Strapi product updates, new features and general improvements.
|
||||||
|
|
||||||
|
Feel free to check out the [Strapi GitHub repository](https://github.com/strapi/strapi). Your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## ✨ Community
|
||||||
|
|
||||||
|
- [Discord](https://discord.strapi.io) - Come chat with the Strapi community including the core team.
|
||||||
|
- [Forum](https://forum.strapi.io/) - Place to discuss, ask questions and find answers, show your Strapi project and get feedback or just talk with other Community members.
|
||||||
|
- [Awesome Strapi](https://github.com/strapi/awesome-strapi) - A curated list of awesome things related to Strapi.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
<sub>🤫 Psst! [Strapi is hiring](https://strapi.io/careers).</sub>
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
export default ({ env }) => ({
|
||||||
|
auth: {
|
||||||
|
secret: env('ADMIN_JWT_SECRET'),
|
||||||
|
},
|
||||||
|
apiToken: {
|
||||||
|
salt: env('API_TOKEN_SALT'),
|
||||||
|
},
|
||||||
|
transfer: {
|
||||||
|
token: {
|
||||||
|
salt: env('TRANSFER_TOKEN_SALT'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
secrets: {
|
||||||
|
encryptionKey: env('ENCRYPTION_KEY'),
|
||||||
|
},
|
||||||
|
flags: {
|
||||||
|
nps: env.bool('FLAG_NPS', true),
|
||||||
|
promoteEE: env.bool('FLAG_PROMOTE_EE', true),
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export default {
|
||||||
|
rest: {
|
||||||
|
defaultLimit: 25,
|
||||||
|
maxLimit: 100,
|
||||||
|
withCount: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
export default ({ env }) => {
|
||||||
|
const client = env('DATABASE_CLIENT', 'sqlite');
|
||||||
|
|
||||||
|
const connections = {
|
||||||
|
mysql: {
|
||||||
|
connection: {
|
||||||
|
host: env('DATABASE_HOST', 'localhost'),
|
||||||
|
port: env.int('DATABASE_PORT', 3306),
|
||||||
|
database: env('DATABASE_NAME', 'strapi'),
|
||||||
|
user: env('DATABASE_USERNAME', 'strapi'),
|
||||||
|
password: env('DATABASE_PASSWORD', 'strapi'),
|
||||||
|
ssl: env.bool('DATABASE_SSL', false) && {
|
||||||
|
key: env('DATABASE_SSL_KEY', undefined),
|
||||||
|
cert: env('DATABASE_SSL_CERT', undefined),
|
||||||
|
ca: env('DATABASE_SSL_CA', undefined),
|
||||||
|
capath: env('DATABASE_SSL_CAPATH', undefined),
|
||||||
|
cipher: env('DATABASE_SSL_CIPHER', undefined),
|
||||||
|
rejectUnauthorized: env.bool('DATABASE_SSL_REJECT_UNAUTHORIZED', true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) },
|
||||||
|
},
|
||||||
|
postgres: {
|
||||||
|
connection: {
|
||||||
|
connectionString: env('DATABASE_URL'),
|
||||||
|
host: env('DATABASE_HOST', 'localhost'),
|
||||||
|
port: env.int('DATABASE_PORT', 5432),
|
||||||
|
database: env('DATABASE_NAME', 'strapi'),
|
||||||
|
user: env('DATABASE_USERNAME', 'strapi'),
|
||||||
|
password: env('DATABASE_PASSWORD', 'strapi'),
|
||||||
|
ssl: env.bool('DATABASE_SSL', false) && {
|
||||||
|
key: env('DATABASE_SSL_KEY', undefined),
|
||||||
|
cert: env('DATABASE_SSL_CERT', undefined),
|
||||||
|
ca: env('DATABASE_SSL_CA', undefined),
|
||||||
|
capath: env('DATABASE_SSL_CAPATH', undefined),
|
||||||
|
cipher: env('DATABASE_SSL_CIPHER', undefined),
|
||||||
|
rejectUnauthorized: env.bool('DATABASE_SSL_REJECT_UNAUTHORIZED', true),
|
||||||
|
},
|
||||||
|
schema: env('DATABASE_SCHEMA', 'public'),
|
||||||
|
},
|
||||||
|
pool: { min: env.int('DATABASE_POOL_MIN', 2), max: env.int('DATABASE_POOL_MAX', 10) },
|
||||||
|
},
|
||||||
|
sqlite: {
|
||||||
|
connection: {
|
||||||
|
filename: path.join(__dirname, '..', '..', env('DATABASE_FILENAME', '.tmp/data.db')),
|
||||||
|
},
|
||||||
|
useNullAsDefault: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
connection: {
|
||||||
|
client,
|
||||||
|
...connections[client],
|
||||||
|
acquireConnectionTimeout: env.int('DATABASE_CONNECTION_TIMEOUT', 60000),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
export default [
|
||||||
|
'strapi::logger',
|
||||||
|
'strapi::errors',
|
||||||
|
'strapi::security',
|
||||||
|
'strapi::cors',
|
||||||
|
'strapi::poweredBy',
|
||||||
|
'strapi::query',
|
||||||
|
'strapi::body',
|
||||||
|
'strapi::session',
|
||||||
|
'strapi::favicon',
|
||||||
|
'strapi::public',
|
||||||
|
];
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export default () => ({});
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
export default ({ env }) => ({
|
||||||
|
host: env('HOST', '0.0.0.0'),
|
||||||
|
port: env.int('PORT', 1337),
|
||||||
|
app: {
|
||||||
|
keys: env.array('APP_KEYS'),
|
||||||
|
},
|
||||||
|
});
|
||||||
Whitespace-only changes.
Binary file not shown.
|
After Width: | Height: | Size: 497 B |
Generated
+19850
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,41 @@
|
|||||||
|
{
|
||||||
|
"name": "cms",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"description": "A Strapi application",
|
||||||
|
"scripts": {
|
||||||
|
"build": "strapi build",
|
||||||
|
"console": "strapi console",
|
||||||
|
"deploy": "strapi deploy",
|
||||||
|
"dev": "strapi develop",
|
||||||
|
"develop": "strapi develop",
|
||||||
|
"start": "strapi start",
|
||||||
|
"strapi": "strapi",
|
||||||
|
"upgrade": "npx @strapi/upgrade latest",
|
||||||
|
"upgrade:dry": "npx @strapi/upgrade latest --dry"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@strapi/plugin-cloud": "5.33.0",
|
||||||
|
"@strapi/plugin-users-permissions": "5.33.0",
|
||||||
|
"@strapi/strapi": "5.33.0",
|
||||||
|
"better-sqlite3": "12.4.1",
|
||||||
|
"react": "^18.0.0",
|
||||||
|
"react-dom": "^18.0.0",
|
||||||
|
"react-router-dom": "^6.0.0",
|
||||||
|
"styled-components": "^6.0.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20",
|
||||||
|
"@types/react": "^18",
|
||||||
|
"@types/react-dom": "^18",
|
||||||
|
"typescript": "^5"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20.0.0 <=24.x.x",
|
||||||
|
"npm": ">=6.0.0"
|
||||||
|
},
|
||||||
|
"strapi": {
|
||||||
|
"uuid": "74121c15-d347-4f01-badc-d66ba28b74c0",
|
||||||
|
"installId": "a70131477a93ad947c033acfccd0d7c9aacc5c05dbf488099d6edb490fd31246"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
# To prevent search engines from seeing the site altogether, uncomment the next two lines:
|
||||||
|
# User-Agent: *
|
||||||
|
# Disallow: /
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import type { StrapiApp } from '@strapi/strapi/admin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
config: {
|
||||||
|
locales: [
|
||||||
|
// 'ar',
|
||||||
|
// 'fr',
|
||||||
|
// 'cs',
|
||||||
|
// 'de',
|
||||||
|
// 'dk',
|
||||||
|
// 'es',
|
||||||
|
// 'he',
|
||||||
|
// 'id',
|
||||||
|
// 'it',
|
||||||
|
// 'ja',
|
||||||
|
// 'ko',
|
||||||
|
// 'ms',
|
||||||
|
// 'nl',
|
||||||
|
// 'no',
|
||||||
|
// 'pl',
|
||||||
|
// 'pt-BR',
|
||||||
|
// 'pt',
|
||||||
|
// 'ru',
|
||||||
|
// 'sk',
|
||||||
|
// 'sv',
|
||||||
|
// 'th',
|
||||||
|
// 'tr',
|
||||||
|
// 'uk',
|
||||||
|
// 'vi',
|
||||||
|
// 'zh-Hans',
|
||||||
|
// 'zh',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
bootstrap(app: StrapiApp) {
|
||||||
|
console.log(app);
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["DOM", "DOM.Iterable", "ESNext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
|
"strict": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx"
|
||||||
|
},
|
||||||
|
"include": ["../plugins/**/admin/src/**/*", "./"],
|
||||||
|
"exclude": ["node_modules/", "build/", "dist/", "**/*.test.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { mergeConfig, type UserConfig } from 'vite';
|
||||||
|
|
||||||
|
export default (config: UserConfig) => {
|
||||||
|
// Important: always return the modified config
|
||||||
|
return mergeConfig(config, {
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': '/src',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
Whitespace-only changes.
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"kind": "collectionType",
|
||||||
|
"collectionName": "articles",
|
||||||
|
"info": {
|
||||||
|
"singularName": "article",
|
||||||
|
"pluralName": "articles",
|
||||||
|
"displayName": "article"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": true
|
||||||
|
},
|
||||||
|
"pluginOptions": {},
|
||||||
|
"attributes": {
|
||||||
|
"title": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"type": "richtext"
|
||||||
|
},
|
||||||
|
"media": {
|
||||||
|
"type": "media",
|
||||||
|
"multiple": true,
|
||||||
|
"allowedTypes": [
|
||||||
|
"images",
|
||||||
|
"files",
|
||||||
|
"videos",
|
||||||
|
"audios"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"author": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* article controller
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreController('api::article.article');
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* article router
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreRouter('api::article.article');
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* article service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreService('api::article.article');
|
||||||
Whitespace-only changes.
@@ -0,0 +1,20 @@
|
|||||||
|
// import type { Core } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/**
|
||||||
|
* An asynchronous register function that runs before
|
||||||
|
* your application is initialized.
|
||||||
|
*
|
||||||
|
* This gives you an opportunity to extend code.
|
||||||
|
*/
|
||||||
|
register(/* { strapi }: { strapi: Core.Strapi } */) {},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An asynchronous bootstrap function that runs before
|
||||||
|
* your application gets started.
|
||||||
|
*
|
||||||
|
* This gives you an opportunity to set up your data model,
|
||||||
|
* run jobs, or perform some special logic.
|
||||||
|
*/
|
||||||
|
bootstrap(/* { strapi }: { strapi: Core.Strapi } */) {},
|
||||||
|
};
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "CommonJS",
|
||||||
|
"moduleResolution": "Node",
|
||||||
|
"lib": ["ES2020"],
|
||||||
|
"target": "ES2019",
|
||||||
|
"strict": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"incremental": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"noEmitOnError": true,
|
||||||
|
"noImplicitThis": true,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "."
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
// Include root files
|
||||||
|
"./",
|
||||||
|
// Include all ts files
|
||||||
|
"./**/*.ts",
|
||||||
|
// Include all js files
|
||||||
|
"./**/*.js",
|
||||||
|
// Force the JSON files in the src folder to be included
|
||||||
|
"src/**/*.json"
|
||||||
|
],
|
||||||
|
|
||||||
|
"exclude": [
|
||||||
|
"node_modules/",
|
||||||
|
"build/",
|
||||||
|
"dist/",
|
||||||
|
".cache/",
|
||||||
|
".tmp/",
|
||||||
|
|
||||||
|
// Do not include admin files in the server compilation
|
||||||
|
"src/admin/",
|
||||||
|
// Do not include test files
|
||||||
|
"**/*.test.*",
|
||||||
|
// Do not include plugins in the server compilation
|
||||||
|
"src/plugins/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
/*
|
||||||
|
* The app doesn't have any components yet.
|
||||||
|
*/
|
||||||
+990
@@ -0,0 +1,990 @@
|
|||||||
|
import type { Schema, Struct } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export interface AdminApiToken extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'strapi_api_tokens';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'Api Token';
|
||||||
|
name: 'Api Token';
|
||||||
|
pluralName: 'api-tokens';
|
||||||
|
singularName: 'api-token';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
accessKey: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
description: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}> &
|
||||||
|
Schema.Attribute.DefaultTo<''>;
|
||||||
|
encryptedKey: Schema.Attribute.Text &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
expiresAt: Schema.Attribute.DateTime;
|
||||||
|
lastUsedAt: Schema.Attribute.DateTime;
|
||||||
|
lifespan: Schema.Attribute.BigInteger;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<'oneToMany', 'admin::api-token'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
name: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Unique &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
permissions: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'admin::api-token-permission'
|
||||||
|
>;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
type: Schema.Attribute.Enumeration<['read-only', 'full-access', 'custom']> &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.DefaultTo<'read-only'>;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminApiTokenPermission extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'strapi_api_token_permissions';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'API Token Permission';
|
||||||
|
name: 'API Token Permission';
|
||||||
|
pluralName: 'api-token-permissions';
|
||||||
|
singularName: 'api-token-permission';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
action: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'admin::api-token-permission'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
token: Schema.Attribute.Relation<'manyToOne', 'admin::api-token'>;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminPermission extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'admin_permissions';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'Permission';
|
||||||
|
name: 'Permission';
|
||||||
|
pluralName: 'permissions';
|
||||||
|
singularName: 'permission';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
action: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
actionParameters: Schema.Attribute.JSON & Schema.Attribute.DefaultTo<{}>;
|
||||||
|
conditions: Schema.Attribute.JSON & Schema.Attribute.DefaultTo<[]>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<'oneToMany', 'admin::permission'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
properties: Schema.Attribute.JSON & Schema.Attribute.DefaultTo<{}>;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
role: Schema.Attribute.Relation<'manyToOne', 'admin::role'>;
|
||||||
|
subject: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminRole extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'admin_roles';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'Role';
|
||||||
|
name: 'Role';
|
||||||
|
pluralName: 'roles';
|
||||||
|
singularName: 'role';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
code: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Unique &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
description: Schema.Attribute.String;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<'oneToMany', 'admin::role'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
name: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Unique &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
permissions: Schema.Attribute.Relation<'oneToMany', 'admin::permission'>;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
users: Schema.Attribute.Relation<'manyToMany', 'admin::user'>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminSession extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'strapi_sessions';
|
||||||
|
info: {
|
||||||
|
description: 'Session Manager storage';
|
||||||
|
displayName: 'Session';
|
||||||
|
name: 'Session';
|
||||||
|
pluralName: 'sessions';
|
||||||
|
singularName: 'session';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
i18n: {
|
||||||
|
localized: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
absoluteExpiresAt: Schema.Attribute.DateTime & Schema.Attribute.Private;
|
||||||
|
childId: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
deviceId: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
expiresAt: Schema.Attribute.DateTime &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<'oneToMany', 'admin::session'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
origin: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
sessionId: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Private &
|
||||||
|
Schema.Attribute.Unique;
|
||||||
|
status: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
type: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
userId: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminTransferToken extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'strapi_transfer_tokens';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'Transfer Token';
|
||||||
|
name: 'Transfer Token';
|
||||||
|
pluralName: 'transfer-tokens';
|
||||||
|
singularName: 'transfer-token';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
accessKey: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
description: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}> &
|
||||||
|
Schema.Attribute.DefaultTo<''>;
|
||||||
|
expiresAt: Schema.Attribute.DateTime;
|
||||||
|
lastUsedAt: Schema.Attribute.DateTime;
|
||||||
|
lifespan: Schema.Attribute.BigInteger;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'admin::transfer-token'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
name: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Unique &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
permissions: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'admin::transfer-token-permission'
|
||||||
|
>;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminTransferTokenPermission
|
||||||
|
extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'strapi_transfer_token_permissions';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'Transfer Token Permission';
|
||||||
|
name: 'Transfer Token Permission';
|
||||||
|
pluralName: 'transfer-token-permissions';
|
||||||
|
singularName: 'transfer-token-permission';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
action: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'admin::transfer-token-permission'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
token: Schema.Attribute.Relation<'manyToOne', 'admin::transfer-token'>;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdminUser extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'admin_users';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'User';
|
||||||
|
name: 'User';
|
||||||
|
pluralName: 'users';
|
||||||
|
singularName: 'user';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
blocked: Schema.Attribute.Boolean &
|
||||||
|
Schema.Attribute.Private &
|
||||||
|
Schema.Attribute.DefaultTo<false>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
email: Schema.Attribute.Email &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Private &
|
||||||
|
Schema.Attribute.Unique &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 6;
|
||||||
|
}>;
|
||||||
|
firstname: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
isActive: Schema.Attribute.Boolean &
|
||||||
|
Schema.Attribute.Private &
|
||||||
|
Schema.Attribute.DefaultTo<false>;
|
||||||
|
lastname: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<'oneToMany', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
password: Schema.Attribute.Password &
|
||||||
|
Schema.Attribute.Private &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 6;
|
||||||
|
}>;
|
||||||
|
preferedLanguage: Schema.Attribute.String;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
registrationToken: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
resetPasswordToken: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
roles: Schema.Attribute.Relation<'manyToMany', 'admin::role'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
username: Schema.Attribute.String;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiArticleArticle extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'articles';
|
||||||
|
info: {
|
||||||
|
displayName: 'article';
|
||||||
|
pluralName: 'articles';
|
||||||
|
singularName: 'article';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: true;
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
author: Schema.Attribute.String;
|
||||||
|
content: Schema.Attribute.RichText;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'api::article.article'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
media: Schema.Attribute.Media<
|
||||||
|
'images' | 'files' | 'videos' | 'audios',
|
||||||
|
true
|
||||||
|
>;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
title: Schema.Attribute.String;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginContentReleasesRelease
|
||||||
|
extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'strapi_releases';
|
||||||
|
info: {
|
||||||
|
displayName: 'Release';
|
||||||
|
pluralName: 'releases';
|
||||||
|
singularName: 'release';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
actions: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::content-releases.release-action'
|
||||||
|
>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::content-releases.release'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
name: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
releasedAt: Schema.Attribute.DateTime;
|
||||||
|
scheduledAt: Schema.Attribute.DateTime;
|
||||||
|
status: Schema.Attribute.Enumeration<
|
||||||
|
['ready', 'blocked', 'failed', 'done', 'empty']
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Required;
|
||||||
|
timezone: Schema.Attribute.String;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginContentReleasesReleaseAction
|
||||||
|
extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'strapi_release_actions';
|
||||||
|
info: {
|
||||||
|
displayName: 'Release Action';
|
||||||
|
pluralName: 'release-actions';
|
||||||
|
singularName: 'release-action';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
contentType: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
entryDocumentId: Schema.Attribute.String;
|
||||||
|
isEntryValid: Schema.Attribute.Boolean;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::content-releases.release-action'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
release: Schema.Attribute.Relation<
|
||||||
|
'manyToOne',
|
||||||
|
'plugin::content-releases.release'
|
||||||
|
>;
|
||||||
|
type: Schema.Attribute.Enumeration<['publish', 'unpublish']> &
|
||||||
|
Schema.Attribute.Required;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginI18NLocale extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'i18n_locale';
|
||||||
|
info: {
|
||||||
|
collectionName: 'locales';
|
||||||
|
description: '';
|
||||||
|
displayName: 'Locale';
|
||||||
|
pluralName: 'locales';
|
||||||
|
singularName: 'locale';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
code: Schema.Attribute.String & Schema.Attribute.Unique;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::i18n.locale'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
name: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.SetMinMax<
|
||||||
|
{
|
||||||
|
max: 50;
|
||||||
|
min: 1;
|
||||||
|
},
|
||||||
|
number
|
||||||
|
>;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginReviewWorkflowsWorkflow
|
||||||
|
extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'strapi_workflows';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'Workflow';
|
||||||
|
name: 'Workflow';
|
||||||
|
pluralName: 'workflows';
|
||||||
|
singularName: 'workflow';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
contentTypes: Schema.Attribute.JSON &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.DefaultTo<'[]'>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::review-workflows.workflow'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
name: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Unique;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
stageRequiredToPublish: Schema.Attribute.Relation<
|
||||||
|
'oneToOne',
|
||||||
|
'plugin::review-workflows.workflow-stage'
|
||||||
|
>;
|
||||||
|
stages: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::review-workflows.workflow-stage'
|
||||||
|
>;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginReviewWorkflowsWorkflowStage
|
||||||
|
extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'strapi_workflows_stages';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'Stages';
|
||||||
|
name: 'Workflow Stage';
|
||||||
|
pluralName: 'workflow-stages';
|
||||||
|
singularName: 'workflow-stage';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
version: '1.1.0';
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
color: Schema.Attribute.String & Schema.Attribute.DefaultTo<'#4945FF'>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::review-workflows.workflow-stage'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
name: Schema.Attribute.String;
|
||||||
|
permissions: Schema.Attribute.Relation<'manyToMany', 'admin::permission'>;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
workflow: Schema.Attribute.Relation<
|
||||||
|
'manyToOne',
|
||||||
|
'plugin::review-workflows.workflow'
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginUploadFile extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'files';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'File';
|
||||||
|
pluralName: 'files';
|
||||||
|
singularName: 'file';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
alternativeText: Schema.Attribute.Text;
|
||||||
|
caption: Schema.Attribute.Text;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
ext: Schema.Attribute.String;
|
||||||
|
folder: Schema.Attribute.Relation<'manyToOne', 'plugin::upload.folder'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
folderPath: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Private &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
formats: Schema.Attribute.JSON;
|
||||||
|
hash: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
height: Schema.Attribute.Integer;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::upload.file'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
mime: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
name: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
previewUrl: Schema.Attribute.Text;
|
||||||
|
provider: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
provider_metadata: Schema.Attribute.JSON;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
related: Schema.Attribute.Relation<'morphToMany'>;
|
||||||
|
size: Schema.Attribute.Decimal & Schema.Attribute.Required;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
url: Schema.Attribute.Text & Schema.Attribute.Required;
|
||||||
|
width: Schema.Attribute.Integer;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginUploadFolder extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'upload_folders';
|
||||||
|
info: {
|
||||||
|
displayName: 'Folder';
|
||||||
|
pluralName: 'folders';
|
||||||
|
singularName: 'folder';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
children: Schema.Attribute.Relation<'oneToMany', 'plugin::upload.folder'>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
files: Schema.Attribute.Relation<'oneToMany', 'plugin::upload.file'>;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::upload.folder'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
name: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
parent: Schema.Attribute.Relation<'manyToOne', 'plugin::upload.folder'>;
|
||||||
|
path: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 1;
|
||||||
|
}>;
|
||||||
|
pathId: Schema.Attribute.Integer &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Unique;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginUsersPermissionsPermission
|
||||||
|
extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'up_permissions';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'Permission';
|
||||||
|
name: 'permission';
|
||||||
|
pluralName: 'permissions';
|
||||||
|
singularName: 'permission';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
action: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::users-permissions.permission'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
role: Schema.Attribute.Relation<
|
||||||
|
'manyToOne',
|
||||||
|
'plugin::users-permissions.role'
|
||||||
|
>;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginUsersPermissionsRole
|
||||||
|
extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'up_roles';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'Role';
|
||||||
|
name: 'role';
|
||||||
|
pluralName: 'roles';
|
||||||
|
singularName: 'role';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
'content-manager': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
'content-type-builder': {
|
||||||
|
visible: false;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
description: Schema.Attribute.String;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::users-permissions.role'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
name: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 3;
|
||||||
|
}>;
|
||||||
|
permissions: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::users-permissions.permission'
|
||||||
|
>;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
type: Schema.Attribute.String & Schema.Attribute.Unique;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
users: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::users-permissions.user'
|
||||||
|
>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PluginUsersPermissionsUser
|
||||||
|
extends Struct.CollectionTypeSchema {
|
||||||
|
collectionName: 'up_users';
|
||||||
|
info: {
|
||||||
|
description: '';
|
||||||
|
displayName: 'User';
|
||||||
|
name: 'user';
|
||||||
|
pluralName: 'users';
|
||||||
|
singularName: 'user';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: false;
|
||||||
|
timestamps: true;
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
blocked: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
|
||||||
|
confirmationToken: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
confirmed: Schema.Attribute.Boolean & Schema.Attribute.DefaultTo<false>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
email: Schema.Attribute.Email &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 6;
|
||||||
|
}>;
|
||||||
|
locale: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'plugin::users-permissions.user'
|
||||||
|
> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
password: Schema.Attribute.Password &
|
||||||
|
Schema.Attribute.Private &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 6;
|
||||||
|
}>;
|
||||||
|
provider: Schema.Attribute.String;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
resetPasswordToken: Schema.Attribute.String & Schema.Attribute.Private;
|
||||||
|
role: Schema.Attribute.Relation<
|
||||||
|
'manyToOne',
|
||||||
|
'plugin::users-permissions.role'
|
||||||
|
>;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
username: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.Required &
|
||||||
|
Schema.Attribute.Unique &
|
||||||
|
Schema.Attribute.SetMinMaxLength<{
|
||||||
|
minLength: 3;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
declare module '@strapi/strapi' {
|
||||||
|
export module Public {
|
||||||
|
export interface ContentTypeSchemas {
|
||||||
|
'admin::api-token': AdminApiToken;
|
||||||
|
'admin::api-token-permission': AdminApiTokenPermission;
|
||||||
|
'admin::permission': AdminPermission;
|
||||||
|
'admin::role': AdminRole;
|
||||||
|
'admin::session': AdminSession;
|
||||||
|
'admin::transfer-token': AdminTransferToken;
|
||||||
|
'admin::transfer-token-permission': AdminTransferTokenPermission;
|
||||||
|
'admin::user': AdminUser;
|
||||||
|
'api::article.article': ApiArticleArticle;
|
||||||
|
'plugin::content-releases.release': PluginContentReleasesRelease;
|
||||||
|
'plugin::content-releases.release-action': PluginContentReleasesReleaseAction;
|
||||||
|
'plugin::i18n.locale': PluginI18NLocale;
|
||||||
|
'plugin::review-workflows.workflow': PluginReviewWorkflowsWorkflow;
|
||||||
|
'plugin::review-workflows.workflow-stage': PluginReviewWorkflowsWorkflowStage;
|
||||||
|
'plugin::upload.file': PluginUploadFile;
|
||||||
|
'plugin::upload.folder': PluginUploadFolder;
|
||||||
|
'plugin::users-permissions.permission': PluginUsersPermissionsPermission;
|
||||||
|
'plugin::users-permissions.role': PluginUsersPermissionsRole;
|
||||||
|
'plugin::users-permissions.user': PluginUsersPermissionsUser;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
VITE_API_URL=http://localhost:3000/api/v1
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# React + TypeScript + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the ESLint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
|
||||||
|
// Remove tseslint.configs.recommended and replace with this
|
||||||
|
tseslint.configs.recommendedTypeChecked,
|
||||||
|
// Alternatively, use this for stricter rules
|
||||||
|
tseslint.configs.strictTypeChecked,
|
||||||
|
// Optionally, add this for stylistic rules
|
||||||
|
tseslint.configs.stylisticTypeChecked,
|
||||||
|
|
||||||
|
// Other configs...
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// eslint.config.js
|
||||||
|
import reactX from 'eslint-plugin-react-x'
|
||||||
|
import reactDom from 'eslint-plugin-react-dom'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
// Other configs...
|
||||||
|
// Enable lint rules for React
|
||||||
|
reactX.configs['recommended-typescript'],
|
||||||
|
// Enable lint rules for React DOM
|
||||||
|
reactDom.configs.recommended,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
parserOptions: {
|
||||||
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
|
tsconfigRootDir: import.meta.dirname,
|
||||||
|
},
|
||||||
|
// other options...
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
|
```
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"tailwind": {
|
||||||
|
"css": "src/index.css",
|
||||||
|
"baseColor": "slate",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import js from '@eslint/js'
|
||||||
|
import globals from 'globals'
|
||||||
|
import reactHooks from 'eslint-plugin-react-hooks'
|
||||||
|
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||||
|
import tseslint from 'typescript-eslint'
|
||||||
|
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||||
|
|
||||||
|
export default defineConfig([
|
||||||
|
globalIgnores(['dist']),
|
||||||
|
{
|
||||||
|
files: ['**/*.{ts,tsx}'],
|
||||||
|
extends: [
|
||||||
|
js.configs.recommended,
|
||||||
|
tseslint.configs.recommended,
|
||||||
|
reactHooks.configs.flat.recommended,
|
||||||
|
reactRefresh.configs.vite,
|
||||||
|
],
|
||||||
|
languageOptions: {
|
||||||
|
ecmaVersion: 2020,
|
||||||
|
globals: globals.browser,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
])
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>frontend</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Generated
+4001
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc -b && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint .",
|
||||||
|
"lint:fix": "eslint . --fix",
|
||||||
|
"type-check": "tsc --noEmit",
|
||||||
|
"test": "vitest",
|
||||||
|
"test:ui": "vitest --ui",
|
||||||
|
"test:coverage": "vitest --coverage"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
|
"@tanstack/react-query": "^5.90.14",
|
||||||
|
"@tanstack/react-router": "^1.144.0",
|
||||||
|
"react": "^19.2.0",
|
||||||
|
"react-dom": "^19.2.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@eslint/js": "^9.39.1",
|
||||||
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/react": "^19.2.5",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^5.1.1",
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"eslint": "^9.39.1",
|
||||||
|
"eslint-plugin-react-hooks": "^7.0.1",
|
||||||
|
"eslint-plugin-react-refresh": "^0.4.24",
|
||||||
|
"globals": "^16.5.0",
|
||||||
|
"lucide-react": "^0.562.0",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"tailwindcss": "^4.1.18",
|
||||||
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
"typescript": "~5.9.3",
|
||||||
|
"typescript-eslint": "^8.46.4",
|
||||||
|
"vite": "^7.2.4"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,42 @@
|
|||||||
|
#root {
|
||||||
|
max-width: 1280px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 6em;
|
||||||
|
padding: 1.5em;
|
||||||
|
will-change: filter;
|
||||||
|
transition: filter 300ms;
|
||||||
|
}
|
||||||
|
.logo:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #646cffaa);
|
||||||
|
}
|
||||||
|
.logo.react:hover {
|
||||||
|
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes logo-spin {
|
||||||
|
from {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: no-preference) {
|
||||||
|
a:nth-of-type(2) .logo {
|
||||||
|
animation: logo-spin infinite 20s linear;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.card {
|
||||||
|
padding: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.read-the-docs {
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,56 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default: "bg-primary text-primary-foreground hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-10 px-4 py-2",
|
||||||
|
sm: "h-9 rounded-md px-3",
|
||||||
|
lg: "h-11 rounded-md px-8",
|
||||||
|
icon: "h-10 w-10",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const Card = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"rounded-lg border bg-card text-card-foreground shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
Card.displayName = "Card"
|
||||||
|
|
||||||
|
const CardHeader = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex flex-col space-y-1.5 p-6", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardHeader.displayName = "CardHeader"
|
||||||
|
|
||||||
|
const CardTitle = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLHeadingElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<h3
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"text-2xl font-semibold leading-none tracking-tight",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardTitle.displayName = "CardTitle"
|
||||||
|
|
||||||
|
const CardDescription = React.forwardRef<
|
||||||
|
HTMLParagraphElement,
|
||||||
|
React.HTMLAttributes<HTMLParagraphElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<p
|
||||||
|
ref={ref}
|
||||||
|
className={cn("text-sm text-muted-foreground", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardDescription.displayName = "CardDescription"
|
||||||
|
|
||||||
|
const CardContent = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
|
||||||
|
))
|
||||||
|
CardContent.displayName = "CardContent"
|
||||||
|
|
||||||
|
const CardFooter = React.forwardRef<
|
||||||
|
HTMLDivElement,
|
||||||
|
React.HTMLAttributes<HTMLDivElement>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cn("flex items-center p-6 pt-0", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
CardFooter.displayName = "CardFooter"
|
||||||
|
|
||||||
|
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
/* @tailwind base; */
|
||||||
|
/* @tailwind components; */
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1';
|
||||||
|
|
||||||
|
export interface Article {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
content: string;
|
||||||
|
excerpt: string | null;
|
||||||
|
slug: string;
|
||||||
|
featuredImage: string;
|
||||||
|
tags: string[];
|
||||||
|
status: 'draft' | 'published' | 'archived';
|
||||||
|
views: number;
|
||||||
|
strapiId: string | null;
|
||||||
|
authorId: string | null;
|
||||||
|
categoryId: string | null;
|
||||||
|
author?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
bio: string | null;
|
||||||
|
avatar: string | null;
|
||||||
|
};
|
||||||
|
category?: {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
slug: string;
|
||||||
|
description: string | null;
|
||||||
|
};
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArticlesResponse {
|
||||||
|
data: Article[];
|
||||||
|
total: number;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FindArticlesParams {
|
||||||
|
category?: string;
|
||||||
|
author?: string;
|
||||||
|
tag?: string;
|
||||||
|
status?: 'draft' | 'published' | 'archived';
|
||||||
|
search?: string;
|
||||||
|
page?: number;
|
||||||
|
limit?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchArticles(params: FindArticlesParams = {}): Promise<ArticlesResponse> {
|
||||||
|
console.log('fetchArticles called with params:', params, 'API_BASE_URL:', API_BASE_URL);
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
Object.entries(params).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined) {
|
||||||
|
searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = `${API_BASE_URL}/articles?${searchParams}`;
|
||||||
|
console.log('Fetching from:', url);
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
console.log('Response status:', response.status, 'ok:', response.ok);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch articles');
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
console.log('Response data:', data);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchArticleBySlug(slug: string): Promise<Article> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/articles/slug/${slug}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch article');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchArticleById(id: string): Promise<Article> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/articles/${id}`);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error('Failed to fetch article');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from 'clsx'
|
||||||
|
import { twMerge } from 'tailwind-merge'
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import ReactDOM from 'react-dom/client'
|
||||||
|
import { RouterProvider } from '@tanstack/react-router'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
|
import { router } from './routes'
|
||||||
|
|
||||||
|
const queryClient = new QueryClient()
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<StrictMode>
|
||||||
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<RouterProvider router={router} />
|
||||||
|
</QueryClientProvider>
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import * as api from '../lib/api';
|
||||||
|
|
||||||
|
export function useArticles(params: api.FindArticlesParams = {}) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['articles', params],
|
||||||
|
queryFn: () => api.fetchArticles(params),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useArticle(slug: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['article', slug],
|
||||||
|
queryFn: () => api.fetchArticleBySlug(slug),
|
||||||
|
enabled: !!slug,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useArticleById(id: string) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['article', id],
|
||||||
|
queryFn: () => api.fetchArticleById(id),
|
||||||
|
enabled: !!id,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,212 @@
|
|||||||
|
import { createRootRoute, createRoute, createRouter, Outlet, Link } from '@tanstack/react-router'
|
||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import * as api from './lib/api'
|
||||||
|
import './styles.css'
|
||||||
|
|
||||||
|
const rootRoute = createRootRoute({
|
||||||
|
head: () => ({
|
||||||
|
meta: [
|
||||||
|
{
|
||||||
|
title: 'Placebo.mk - Sarcastic News from Macedonia',
|
||||||
|
description: 'Latest news and articles from Macedonia with a sarcastic twist',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
component: () => (
|
||||||
|
<div className="min-h-screen bg-background text-foreground flex flex-col">
|
||||||
|
<header className="border-b">
|
||||||
|
<div className="container mx-auto max-w-6xl px-4 py-4">
|
||||||
|
<h1 className="text-3xl font-bold">
|
||||||
|
<Link to="/">Placebo.mk</Link>
|
||||||
|
</h1>
|
||||||
|
<nav className="flex gap-4">
|
||||||
|
<Link to="/" className="text-sm font-medium hover:underline">
|
||||||
|
Home
|
||||||
|
</Link>
|
||||||
|
<Link to="/articles" className="text-sm font-medium hover:underline">
|
||||||
|
Articles
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main className="flex-1 container mx-auto max-w-6xl px-4 py-8">
|
||||||
|
<Outlet />
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="border-t mt-12">
|
||||||
|
<div className="container mx-auto max-w-6xl px-4 py-6 text-center text-sm text-muted-foreground">
|
||||||
|
© 2025 Placebo.mk. Sarcastic news from Macedonia.
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const indexRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/',
|
||||||
|
component: () => (
|
||||||
|
<div className="py-12 md:py-20">
|
||||||
|
<div className="max-w-4xl mx-auto text-center mb-12">
|
||||||
|
<div className="inline-flex items-center justify-center w-16 h-16 rounded-full bg-primary/10 mb-6">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
|
<path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z" />
|
||||||
|
<polyline points="14 2 14 8 20 8" />
|
||||||
|
<line x1="16" x2="8" y1="13" y2="13" />
|
||||||
|
<line x1="16" x2="8" y1="17" y2="17" />
|
||||||
|
<line x1="10" x2="8" y1="9" y2="9" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold tracking-tight mb-4">
|
||||||
|
Placebo<span className="text-primary">.mk</span>
|
||||||
|
</h1>
|
||||||
|
<p className="text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
Unapologetically sarcastic news and commentary on local and global affairs in Macedonia. Because sometimes the truth hurts more than fiction.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid md:grid-cols-3 gap-6 max-w-5xl mx-auto">
|
||||||
|
<div className="p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 group">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
|
<path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-2 2Zm0 0a2 2 0 0 1-2-2v-9c0-1.1.9-2 2-2h2" />
|
||||||
|
<path d="M18 14h-8" />
|
||||||
|
<path d="M15 18h-5" />
|
||||||
|
<path d="M10 6h8v4h-8V6Z" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Latest Articles</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Freshly brewed sarcasm on current events, politics, and everything in between.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 group">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
|
<circle cx="12" cy="12" r="10" />
|
||||||
|
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
|
||||||
|
<path d="M12 17h.01" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">No Filter</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
We don't do nuance. We don't do diplomatic language. Just honest (and slightly mean) commentary.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="p-6 rounded-xl border bg-card hover:shadow-lg transition-all duration-300 group">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-primary/10 flex items-center justify-center mb-4 group-hover:bg-primary/20 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="text-primary">
|
||||||
|
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2" />
|
||||||
|
<circle cx="9" cy="7" r="4" />
|
||||||
|
<path d="M23 21v-2a4 4 0 0 0-3-3.87" />
|
||||||
|
<path d="M16 3.13a4 4 0 0 1 0 7.75" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 className="text-lg font-semibold mb-2">Community</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Join thousands of readers who appreciate the finer art of Macedonian sarcasm.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-16 text-center">
|
||||||
|
<Link
|
||||||
|
to="/articles"
|
||||||
|
className="inline-flex items-center gap-2 px-6 py-3 rounded-lg bg-primary text-primary-foreground font-medium hover:bg-primary/90 transition-colors"
|
||||||
|
>
|
||||||
|
Browse Articles
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
<path d="m12 5 7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
const articlesRoute = createRoute({
|
||||||
|
getParentRoute: () => rootRoute,
|
||||||
|
path: '/articles',
|
||||||
|
component: () => {
|
||||||
|
const { data, isLoading, error } = useQuery({
|
||||||
|
queryKey: ['articles'],
|
||||||
|
queryFn: () => api.fetchArticles({ status: 'published' }),
|
||||||
|
})
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="text-lg">Loading articles...</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
|
<div className="text-lg text-red-500">Error loading articles</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="mb-8">
|
||||||
|
<h1 className="text-3xl font-bold">Articles</h1>
|
||||||
|
<p className="text-muted-foreground">Latest news and articles</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
{data?.data.map((article) => (
|
||||||
|
<div
|
||||||
|
key={article.id}
|
||||||
|
className="p-6 rounded-xl border bg-card hover:shadow-lg transition-shadow"
|
||||||
|
>
|
||||||
|
<h2 className="text-xl font-semibold mb-2 line-clamp-2">
|
||||||
|
{article.title}
|
||||||
|
</h2>
|
||||||
|
{article.excerpt && (
|
||||||
|
<p className="text-muted-foreground text-sm mb-4 line-clamp-3">
|
||||||
|
{article.excerpt}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="flex items-center justify-between text-sm text-muted-foreground">
|
||||||
|
<span>
|
||||||
|
{new Date(article.createdAt).toLocaleDateString('mk-MK', {
|
||||||
|
day: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
year: 'numeric',
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span>{article.views} views</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{data?.data.length === 0 && (
|
||||||
|
<div className="text-center py-12">
|
||||||
|
<p className="text-muted-foreground text-lg">
|
||||||
|
No articles published yet. Check back soon!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const routeTree = rootRoute.addChildren([indexRoute, articlesRoute])
|
||||||
|
|
||||||
|
export const router = createRouter({ routeTree })
|
||||||
|
|
||||||
|
declare module '@tanstack/react-router' {
|
||||||
|
interface Register {
|
||||||
|
router: typeof router
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--color-primary: oklch(0.647 0.22 0.23);
|
||||||
|
--color-primary-foreground: oklch(0.985 0.002 0);
|
||||||
|
--color-secondary: oklch(0.97 0.002 0);
|
||||||
|
--color-secondary-foreground: oklch(0.205 0.02 266.5);
|
||||||
|
--color-muted: oklch(0.97 0 0);
|
||||||
|
--color-muted-foreground: oklch(0.556 0 0);
|
||||||
|
--color-accent: oklch(0.97 0 0);
|
||||||
|
--color-accent-foreground: oklch(0.205 0.02 266.5);
|
||||||
|
--color-destructive: oklch(0.55 0.22 25);
|
||||||
|
--color-destructive-foreground: oklch(0.985 0.002 0);
|
||||||
|
--color-border: oklch(0.9 0 0);
|
||||||
|
--color-input: oklch(0.9 0 0);
|
||||||
|
--color-ring: oklch(0.647 0.22 0.23);
|
||||||
|
--radius: 0.5rem;
|
||||||
|
--font-sans: "Inter", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: 0 0% 100%;
|
||||||
|
--foreground: 222.2 84% 4.9%;
|
||||||
|
--card: 0 0% 100%;
|
||||||
|
--card-foreground: 222.2 84% 4.9%;
|
||||||
|
--popover: 0 0% 100%;
|
||||||
|
--popover-foreground: 222.2 84% 4.9%;
|
||||||
|
--primary: 222.2 47.4% 11.2%;
|
||||||
|
--primary-foreground: 210 40% 98%;
|
||||||
|
--secondary: 210 40% 96.1%;
|
||||||
|
--secondary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--muted: 210 40% 96.1%;
|
||||||
|
--muted-foreground: 215.4 16.3% 46.9%;
|
||||||
|
--accent: 210 40% 96.1%;
|
||||||
|
--accent-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--destructive: 0 84.2% 60.2%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 214.3 31.8% 91.4%;
|
||||||
|
--input: 214.3 31.8% 91.4%;
|
||||||
|
--ring: 222.2 84% 4.9%;
|
||||||
|
--radius: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: 222.2 84% 4.9%;
|
||||||
|
--foreground: 210 40% 98%;
|
||||||
|
--card: 222.2 84% 4.9%;
|
||||||
|
--card-foreground: 210 40% 98%;
|
||||||
|
--popover: 222.2 84% 4.9%;
|
||||||
|
--popover-foreground: 210 40% 98%;
|
||||||
|
--primary: 210 40% 98%;
|
||||||
|
--primary-foreground: 222.2 47.4% 11.2%;
|
||||||
|
--secondary: 217.2 32.6% 17.5%;
|
||||||
|
--secondary-foreground: 210 40% 98%;
|
||||||
|
--muted: 217.2 32.6% 17.5%;
|
||||||
|
--muted-foreground: 215 20.2% 65.1%;
|
||||||
|
--accent: 217.2 32.6% 17.5%;
|
||||||
|
--accent-foreground: 210 40% 98%;
|
||||||
|
--destructive: 0 62.8% 30.6%;
|
||||||
|
--destructive-foreground: 210 40% 98%;
|
||||||
|
--border: 217.2 32.6% 17.5%;
|
||||||
|
--input: 217.2 32.6% 17.5%;
|
||||||
|
--ring: 212.7 26.8% 83.9%;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
border-color: hsl(var(--border));
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
background-color: hsl(var(--background));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["vite/client"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
|
/* Path aliases */
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
"target": "ES2023",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"types": ["node"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
|
||||||
|
/* Bundler mode */
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
|
||||||
|
/* Linting */
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"erasableSyntaxOnly": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"noUncheckedSideEffectImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
import tailwindcss from '@tailwindcss/vite'
|
||||||
|
import path from 'path'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
tailwindcss(),
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
## placebo.mk - дневна доза добри вести
|
||||||
|
|
||||||
|
# Description
|
||||||
|
placebo.mk is a news site in macedonia that covers current local and global affair in
|
||||||
|
sarcastic tone.
|
||||||
|
|
||||||
|
# Design principles
|
||||||
|
web site have minimalistic and clean design
|
||||||
|
|
||||||
|
# Tech stack
|
||||||
|
# frontend
|
||||||
|
tanstack
|
||||||
|
# backend
|
||||||
|
nestjs
|
||||||
|
# database
|
||||||
|
sqlite
|
||||||
|
# CMS
|
||||||
|
|
||||||
Reference in new issue
Block a user