Clean up root dir

This commit is contained in:
myrmidex 2025-08-03 21:11:19 +02:00
parent da857b7951
commit a7108ce17c
48 changed files with 0 additions and 4664 deletions

View file

@ -1,66 +0,0 @@
# Multi-stage build for Laravel with React frontend
FROM node:22-alpine AS frontend-builder
WORKDIR /app
COPY package*.json ./
RUN npm install --only=production
COPY . .
RUN npm run build
FROM php:8.4-fpm-alpine
# Install system dependencies and PHP extensions
RUN apk add --no-cache \
git \
curl \
libpng-dev \
oniguruma-dev \
libxml2-dev \
zip \
unzip \
autoconf \
gcc \
g++ \
make \
gettext \
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd \
&& pecl install redis \
&& docker-php-ext-enable redis
# Install Composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
# Set working directory
WORKDIR /var/www/html
# Copy composer files
COPY composer*.json ./
# Copy application files (needed for artisan in composer scripts)
COPY . .
# Install dependencies
RUN composer install --no-dev --optimize-autoloader --no-interaction --ignore-platform-reqs
# Copy production environment file and generate APP_KEY
COPY docker/build/laravel.env .env
RUN php artisan key:generate
# Copy built frontend assets
COPY --from=frontend-builder /app/public/build /var/www/html/public/build
# Set permissions
RUN chown -R www-data:www-data /var/www/html \
&& chmod -R 755 /var/www/html/storage \
&& chmod -R 755 /var/www/html/bootstrap/cache
# Create entrypoint script and health check scripts
COPY docker/build/entrypoint.sh /entrypoint.sh
COPY docker/build/wait-for-db.php /docker/wait-for-db.php
COPY docker/build/wait-for-redis.php /docker/wait-for-redis.php
RUN chmod +x /entrypoint.sh /docker/wait-for-db.php /docker/wait-for-redis.php
EXPOSE 8000
ENTRYPOINT ["/entrypoint.sh"]
CMD ["web"]

View file

@ -1,21 +0,0 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "tailwind.config.js",
"css": "resources/css/app.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}

View file

@ -1,73 +0,0 @@
services:
laravel.test:
build:
context: ./vendor/laravel/sail/runtimes/8.4
dockerfile: Dockerfile
args:
WWWGROUP: '${WWWGROUP}'
image: sail-8.4/app
extra_hosts:
- 'host.docker.internal:host-gateway'
ports:
- '${APP_PORT:-80}:80'
- '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
environment:
WWWUSER: '${WWWUSER}'
LARAVEL_SAIL: 1
XDEBUG_MODE: '${SAIL_XDEBUG_MODE:-off}'
XDEBUG_CONFIG: '${SAIL_XDEBUG_CONFIG:-client_host=host.docker.internal}'
IGNITION_LOCAL_SITES_PATH: '${PWD}'
volumes:
- '.:/var/www/html'
networks:
- sail
depends_on:
- mysql
- redis
mysql:
image: 'mysql/mysql-server:8.0'
ports:
- '${FORWARD_DB_PORT:-3306}:3306'
environment:
MYSQL_ROOT_PASSWORD: '${DB_PASSWORD}'
MYSQL_ROOT_HOST: '%'
MYSQL_DATABASE: '${DB_DATABASE}'
MYSQL_USER: '${DB_USERNAME}'
MYSQL_PASSWORD: '${DB_PASSWORD}'
MYSQL_ALLOW_EMPTY_PASSWORD: 1
volumes:
- 'sail-mysql:/var/lib/mysql'
- './vendor/laravel/sail/database/mysql/create-testing-database.sh:/docker-entrypoint-initdb.d/10-create-testing-database.sh'
networks:
- sail
healthcheck:
test:
- CMD
- mysqladmin
- ping
- '-p${DB_PASSWORD}'
retries: 3
timeout: 5s
redis:
image: 'redis:alpine'
ports:
- '${FORWARD_REDIS_PORT:-6379}:6379'
volumes:
- 'sail-redis:/data'
networks:
- sail
healthcheck:
test:
- CMD
- redis-cli
- ping
retries: 3
timeout: 5s
networks:
sail:
driver: bridge
volumes:
sail-mysql:
driver: local
sail-redis:
driver: local

View file

@ -1,44 +0,0 @@
import js from '@eslint/js';
import prettier from 'eslint-config-prettier';
import react from 'eslint-plugin-react';
import reactHooks from 'eslint-plugin-react-hooks';
import globals from 'globals';
import typescript from 'typescript-eslint';
/** @type {import('eslint').Linter.Config[]} */
export default [
js.configs.recommended,
...typescript.configs.recommended,
{
...react.configs.flat.recommended,
...react.configs.flat['jsx-runtime'], // Required for React 17+
languageOptions: {
globals: {
...globals.browser,
},
},
rules: {
'react/react-in-jsx-scope': 'off',
'react/prop-types': 'off',
'react/no-unescaped-entities': 'off',
},
settings: {
react: {
version: 'detect',
},
},
},
{
plugins: {
'react-hooks': reactHooks,
},
rules: {
'react-hooks/rules-of-hooks': 'error',
'react-hooks/exhaustive-deps': 'warn',
},
},
{
ignores: ['vendor', 'node_modules', 'public', 'bootstrap/ssr', 'tailwind.config.js'],
},
prettier, // Turn off all rules that might conflict with Prettier
];

View file

@ -1,26 +0,0 @@
{
"private": true,
"scripts": {
"build": "vite build",
"dev": "vite"
},
"devDependencies": {
"@tailwindcss/vite": "^4.0.0",
"laravel-vite-plugin": "^1.0",
"tailwindcss": "^4.0.0",
"tailwindcss-animate": "^1.0.7",
"vite": "^6.0"
},
"dependencies": {
"@tanstack/react-query": "^5.84.1",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"@vitejs/plugin-react": "^4.7.0",
"axios": "^1.11.0",
"lucide-react": "^0.536.0",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-router-dom": "^7.7.1",
"typescript": "^5.9.2"
}
}

View file

@ -1,159 +0,0 @@
@import 'tailwindcss';
@plugin 'tailwindcss-animate';
@source '../views';
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@custom-variant dark (&:is(.dark *));
@theme {
--font-sans:
'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
--radius-lg: var(--radius);
--radius-md: calc(var(--radius) - 2px);
--radius-sm: calc(var(--radius) - 4px);
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}
/*
The default border color has changed to `currentColor` in Tailwind CSS v4,
so we've added these compatibility styles to make sure everything still
looks the same as it did with Tailwind CSS v3.
If we ever want to remove these styles, we need to add an explicit border
color utility to any element that depends on these defaults.
*/
@layer base {
*,
::after,
::before,
::backdrop,
::file-selector-button {
border-color: var(--color-gray-200, currentColor);
}
}
:root {
--background: oklch(1 0 0);
--foreground: oklch(0.145 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.145 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0);
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
--muted: oklch(0.97 0 0);
--muted-foreground: oklch(0.556 0 0);
--accent: oklch(0.97 0 0);
--accent-foreground: oklch(0.205 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.922 0 0);
--input: oklch(0.922 0 0);
--ring: oklch(0.87 0 0);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.87 0 0);
}
.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.985 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View file

@ -1,32 +0,0 @@
import React from 'react';
import { Routes, Route, Navigate } from 'react-router-dom';
import { apiClient } from './lib/api';
import Layout from './components/Layout';
import Login from './pages/Login';
import Dashboard from './pages/Dashboard';
import Articles from './pages/Articles';
import Feeds from './pages/Feeds';
import Settings from './pages/Settings';
const App: React.FC = () => {
const isAuthenticated = apiClient.isAuthenticated();
if (!isAuthenticated) {
return <Login />;
}
return (
<Layout>
<Routes>
<Route path="/" element={<Navigate to="/dashboard" replace />} />
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/articles" element={<Articles />} />
<Route path="/feeds" element={<Feeds />} />
<Route path="/settings" element={<Settings />} />
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</Layout>
);
};
export default App;

View file

@ -1,34 +0,0 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { BrowserRouter } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import App from './App';
import './bootstrap';
// Create React Query client
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
},
},
});
// Get the root element
const container = document.getElementById('app');
if (!container) {
throw new Error('Root element not found');
}
const root = createRoot(container);
root.render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<App />
</BrowserRouter>
</QueryClientProvider>
</React.StrictMode>
);

View file

@ -1,21 +0,0 @@
/**
* Bootstrap file for setting up global configurations
*/
// Set up axios defaults
import axios from 'axios';
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
axios.defaults.headers.common['Accept'] = 'application/json';
axios.defaults.headers.common['Content-Type'] = 'application/json';
// Add CSRF token if available
const token = document.head.querySelector('meta[name="csrf-token"]');
if (token) {
axios.defaults.headers.common['X-CSRF-TOKEN'] = (token as HTMLMetaElement).content;
}
// Set base URL for API calls
axios.defaults.baseURL = '/api/v1';
export default axios;

View file

@ -1,161 +0,0 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import {
Home,
FileText,
Rss,
Settings as SettingsIcon,
LogOut,
Menu,
X
} from 'lucide-react';
import { apiClient } from '../lib/api';
interface LayoutProps {
children: React.ReactNode;
}
const Layout: React.FC<LayoutProps> = ({ children }) => {
const [sidebarOpen, setSidebarOpen] = React.useState(false);
const location = useLocation();
const user = apiClient.getUser();
const navigation = [
{ name: 'Dashboard', href: '/dashboard', icon: Home },
{ name: 'Articles', href: '/articles', icon: FileText },
{ name: 'Feeds', href: '/feeds', icon: Rss },
{ name: 'Settings', href: '/settings', icon: SettingsIcon },
];
const handleLogout = async () => {
try {
await apiClient.logout();
window.location.reload();
} catch (error) {
console.error('Logout failed:', error);
apiClient.clearAuth();
window.location.reload();
}
};
const renderMobileOverlay = () => {
if (!sidebarOpen) return null;
return (
<div
className="fixed inset-0 z-40 bg-gray-600 bg-opacity-75 lg:hidden"
onClick={() => setSidebarOpen(false)}
/>
);
};
return (
<div className="min-h-screen bg-gray-50">
{renderMobileOverlay()}
{/* Mobile sidebar */}
<div className={`fixed inset-y-0 left-0 z-50 w-64 bg-white shadow-lg transform transition-transform duration-300 ease-in-out lg:hidden ${
sidebarOpen ? 'translate-x-0' : '-translate-x-full'
}`}>
<div className="flex items-center justify-between h-16 px-4 border-b border-gray-200">
<h1 className="text-xl font-bold text-gray-900">FFR</h1>
<button
onClick={() => setSidebarOpen(false)}
className="p-2 rounded-md text-gray-400 hover:text-gray-600 hover:bg-gray-100"
>
<X className="h-6 w-6" />
</button>
</div>
<nav className="mt-5 px-2">
{navigation.map((item) => {
const Icon = item.icon;
const isActive = location.pathname === item.href;
return (
<Link
key={item.name}
to={item.href}
className={`group flex items-center px-2 py-2 text-base font-medium rounded-md mb-1 ${
isActive
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`}
onClick={() => setSidebarOpen(false)}
>
<Icon className="mr-4 h-6 w-6" />
{item.name}
</Link>
);
})}
</nav>
</div>
{/* Desktop sidebar */}
<div className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64 lg:flex-col">
<div className="flex flex-col flex-grow bg-white pt-5 pb-4 overflow-y-auto border-r border-gray-200">
<div className="flex items-center flex-shrink-0 px-4">
<h1 className="text-xl font-bold text-gray-900">FFR</h1>
</div>
<nav className="mt-5 flex-1 px-2 bg-white">
{navigation.map((item) => {
const Icon = item.icon;
const isActive = location.pathname === item.href;
return (
<Link
key={item.name}
to={item.href}
className={`group flex items-center px-2 py-2 text-sm font-medium rounded-md mb-1 ${
isActive
? 'bg-blue-100 text-blue-700'
: 'text-gray-600 hover:bg-gray-50 hover:text-gray-900'
}`}
>
<Icon className="mr-3 h-6 w-6" />
{item.name}
</Link>
);
})}
</nav>
<div className="flex-shrink-0 p-4 border-t border-gray-200">
<div className="flex items-center">
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-gray-900 truncate">
{user?.name || 'User'}
</p>
<p className="text-sm text-gray-500 truncate">
{user?.email || 'user@example.com'}
</p>
</div>
<button
onClick={handleLogout}
className="ml-3 p-2 text-gray-400 hover:text-gray-600 hover:bg-gray-100 rounded-md"
title="Logout"
>
<LogOut className="h-5 w-5" />
</button>
</div>
</div>
</div>
</div>
{/* Main content */}
<div className="lg:pl-64 flex flex-col flex-1">
<div className="sticky top-0 z-10 flex-shrink-0 flex h-16 bg-white shadow lg:hidden">
<button
onClick={() => setSidebarOpen(true)}
className="px-4 border-r border-gray-200 text-gray-500 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-blue-500 lg:hidden"
>
<Menu className="h-6 w-6" />
</button>
<div className="flex-1 px-4 flex justify-between items-center">
<h1 className="text-lg font-medium text-gray-900">FFR</h1>
</div>
</div>
<main className="flex-1">
{children}
</main>
</div>
</div>
);
};
export default Layout;

View file

@ -1,288 +0,0 @@
import axios from 'axios';
// Types for API responses
export interface ApiResponse<T = any> {
success: boolean;
data: T;
message: string;
}
export interface ApiError {
success: false;
message: string;
errors?: Record<string, string[]>;
}
export interface PaginatedResponse<T> {
data: T[];
pagination: {
current_page: number;
last_page: number;
per_page: number;
total: number;
from: number | null;
to: number | null;
};
}
// User types
export interface User {
id: number;
name: string;
email: string;
}
export interface LoginCredentials {
email: string;
password: string;
}
export interface RegisterData {
name: string;
email: string;
password: string;
password_confirmation: string;
}
export interface AuthResponse {
user: User;
token: string;
token_type: string;
}
// Article types
export interface Article {
id: number;
feed_id: number;
url: string;
title: string;
description: string;
is_valid: boolean;
is_duplicate: boolean;
approval_status: 'pending' | 'approved' | 'rejected';
approved_at: string | null;
approved_by: string | null;
fetched_at: string | null;
validated_at: string | null;
created_at: string;
updated_at: string;
feed?: Feed;
article_publication?: ArticlePublication;
}
// Feed types
export interface Feed {
id: number;
name: string;
url: string;
type: 'website' | 'rss';
is_active: boolean;
description: string | null;
created_at: string;
updated_at: string;
articles_count?: number;
}
// Other types
export interface ArticlePublication {
id: number;
article_id: number;
status: string;
published_at: string | null;
created_at: string;
updated_at: string;
}
export interface PlatformAccount {
id: number;
platform_instance_id: number;
account_id: string;
username: string;
display_name: string | null;
description: string | null;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface PlatformChannel {
id: number;
platform_instance_id: number;
channel_id: string;
name: string;
display_name: string | null;
description: string | null;
is_active: boolean;
created_at: string;
updated_at: string;
}
export interface Settings {
article_processing_enabled: boolean;
publishing_approvals_enabled: boolean;
}
export interface DashboardStats {
article_stats: {
total_today: number;
total_week: number;
total_month: number;
approved_today: number;
approved_week: number;
approved_month: number;
approval_percentage_today: number;
approval_percentage_week: number;
approval_percentage_month: number;
};
system_stats: {
total_feeds: number;
active_feeds: number;
total_platform_accounts: number;
active_platform_accounts: number;
total_platform_channels: number;
active_platform_channels: number;
total_routes: number;
active_routes: number;
};
available_periods: Array<{ value: string; label: string }>;
current_period: string;
}
// API Client class
class ApiClient {
private token: string | null = null;
constructor() {
// Get token from localStorage if available
this.token = localStorage.getItem('auth_token');
this.setupInterceptors();
}
private setupInterceptors() {
// Request interceptor to add auth token
axios.interceptors.request.use((config) => {
if (this.token) {
config.headers.Authorization = `Bearer ${this.token}`;
}
return config;
});
// Response interceptor to handle errors
axios.interceptors.response.use(
(response) => response,
(error) => {
if (error.response?.status === 401) {
this.clearAuth();
window.location.href = '/login';
}
return Promise.reject(error);
}
);
}
setAuth(token: string, user: User) {
this.token = token;
localStorage.setItem('auth_token', token);
localStorage.setItem('user', JSON.stringify(user));
}
clearAuth() {
this.token = null;
localStorage.removeItem('auth_token');
localStorage.removeItem('user');
}
getUser(): User | null {
const userStr = localStorage.getItem('user');
return userStr ? JSON.parse(userStr) : null;
}
isAuthenticated(): boolean {
return !!this.token;
}
// Auth endpoints
async login(credentials: LoginCredentials): Promise<AuthResponse> {
const response = await axios.post<ApiResponse<AuthResponse>>('/auth/login', credentials);
return response.data.data;
}
async register(data: RegisterData): Promise<AuthResponse> {
const response = await axios.post<ApiResponse<AuthResponse>>('/auth/register', data);
return response.data.data;
}
async logout(): Promise<void> {
await axios.post('/auth/logout');
this.clearAuth();
}
async me(): Promise<User> {
const response = await axios.get<ApiResponse<{ user: User }>>('/auth/me');
return response.data.data.user;
}
// Dashboard endpoints
async getDashboardStats(period = 'today'): Promise<DashboardStats> {
const response = await axios.get<ApiResponse<DashboardStats>>('/dashboard/stats', {
params: { period }
});
return response.data.data;
}
// Articles endpoints
async getArticles(page = 1, perPage = 15): Promise<{ articles: Article[]; pagination: any; settings: any }> {
const response = await axios.get<ApiResponse<{ articles: Article[]; pagination: any; settings: any }>>('/articles', {
params: { page, per_page: perPage }
});
return response.data.data;
}
async approveArticle(articleId: number): Promise<Article> {
const response = await axios.post<ApiResponse<Article>>(`/articles/${articleId}/approve`);
return response.data.data;
}
async rejectArticle(articleId: number): Promise<Article> {
const response = await axios.post<ApiResponse<Article>>(`/articles/${articleId}/reject`);
return response.data.data;
}
// Feeds endpoints
async getFeeds(): Promise<Feed[]> {
const response = await axios.get<ApiResponse<Feed[]>>('/feeds');
return response.data.data;
}
async createFeed(data: Partial<Feed>): Promise<Feed> {
const response = await axios.post<ApiResponse<Feed>>('/feeds', data);
return response.data.data;
}
async updateFeed(id: number, data: Partial<Feed>): Promise<Feed> {
const response = await axios.put<ApiResponse<Feed>>(`/feeds/${id}`, data);
return response.data.data;
}
async deleteFeed(id: number): Promise<void> {
await axios.delete(`/feeds/${id}`);
}
async toggleFeed(id: number): Promise<Feed> {
const response = await axios.post<ApiResponse<Feed>>(`/feeds/${id}/toggle`);
return response.data.data;
}
// Settings endpoints
async getSettings(): Promise<Settings> {
const response = await axios.get<ApiResponse<Settings>>('/settings');
return response.data.data;
}
async updateSettings(data: Partial<Settings>): Promise<Settings> {
const response = await axios.put<ApiResponse<Settings>>('/settings', data);
return response.data.data;
}
}
export const apiClient = new ApiClient();

View file

@ -1,249 +0,0 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { CheckCircle, XCircle, ExternalLink, Calendar, Tag, FileText } from 'lucide-react';
import { apiClient, Article } from '../lib/api';
const Articles: React.FC = () => {
const [page, setPage] = useState(1);
const queryClient = useQueryClient();
const { data, isLoading, error } = useQuery({
queryKey: ['articles', page],
queryFn: () => apiClient.getArticles(page),
});
const approveMutation = useMutation({
mutationFn: (articleId: number) => apiClient.approveArticle(articleId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['articles'] });
},
});
const rejectMutation = useMutation({
mutationFn: (articleId: number) => apiClient.rejectArticle(articleId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['articles'] });
},
});
const handleApprove = (articleId: number) => {
approveMutation.mutate(articleId);
};
const handleReject = (articleId: number) => {
rejectMutation.mutate(articleId);
};
const getStatusBadge = (status: string) => {
switch (status) {
case 'approved':
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
<CheckCircle className="h-3 w-3 mr-1" />
Approved
</span>
);
case 'rejected':
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
<XCircle className="h-3 w-3 mr-1" />
Rejected
</span>
);
default:
return (
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
<Calendar className="h-3 w-3 mr-1" />
Pending
</span>
);
}
};
if (isLoading) {
return (
<div className="p-6">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
<div className="space-y-4">
{[...Array(5)].map((_, i) => (
<div key={i} className="bg-white p-6 rounded-lg shadow">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-3 bg-gray-200 rounded w-1/2 mb-4"></div>
<div className="flex space-x-2">
<div className="h-8 bg-gray-200 rounded w-20"></div>
<div className="h-8 bg-gray-200 rounded w-20"></div>
</div>
</div>
))}
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="p-6">
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<p className="text-red-600">Failed to load articles</p>
</div>
</div>
);
}
const articles = data?.articles || [];
const pagination = data?.pagination;
const settings = data?.settings;
return (
<div className="p-6">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Articles</h1>
<p className="mt-1 text-sm text-gray-500">
Manage and review articles from your feeds
</p>
{settings?.publishing_approvals_enabled && (
<div className="mt-2 inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
<Tag className="h-3 w-3 mr-1" />
Approval system enabled
</div>
)}
</div>
<div className="space-y-6">
{articles.map((article: Article) => (
<div key={article.id} className="bg-white rounded-lg shadow p-6">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<h3 className="text-lg font-medium text-gray-900 mb-2">
{article.title || 'Untitled Article'}
</h3>
<p className="text-sm text-gray-600 mb-3 line-clamp-2">
{article.description || 'No description available'}
</p>
<div className="flex items-center space-x-4 text-xs text-gray-500">
<span>Feed: {article.feed?.name || 'Unknown'}</span>
<span></span>
<span>{new Date(article.created_at).toLocaleDateString()}</span>
{article.is_valid !== null && (
<>
<span></span>
<span className={article.is_valid ? 'text-green-600' : 'text-red-600'}>
{article.is_valid ? 'Valid' : 'Invalid'}
</span>
</>
)}
{article.is_duplicate && (
<>
<span></span>
<span className="text-orange-600">Duplicate</span>
</>
)}
</div>
</div>
<div className="flex items-center space-x-3 ml-4">
{getStatusBadge(article.approval_status)}
<a
href={article.url}
target="_blank"
rel="noopener noreferrer"
className="p-2 text-gray-400 hover:text-gray-600 rounded-md"
title="View original article"
>
<ExternalLink className="h-4 w-4" />
</a>
</div>
</div>
{article.approval_status === 'pending' && settings?.publishing_approvals_enabled && (
<div className="mt-4 flex space-x-3">
<button
onClick={() => handleApprove(article.id)}
disabled={approveMutation.isPending}
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50"
>
<CheckCircle className="h-4 w-4 mr-1" />
Approve
</button>
<button
onClick={() => handleReject(article.id)}
disabled={rejectMutation.isPending}
className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-red-600 hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-red-500 disabled:opacity-50"
>
<XCircle className="h-4 w-4 mr-1" />
Reject
</button>
</div>
)}
</div>
))}
{articles.length === 0 && (
<div className="text-center py-12">
<FileText className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No articles</h3>
<p className="mt-1 text-sm text-gray-500">
No articles have been fetched yet.
</p>
</div>
)}
{/* Pagination */}
{pagination && pagination.last_page > 1 && (
<div className="flex items-center justify-between bg-white px-4 py-3 border-t border-gray-200 sm:px-6 rounded-lg shadow">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => setPage(page - 1)}
disabled={page <= 1}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page >= pagination.last_page}
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing{' '}
<span className="font-medium">{pagination.from}</span> to{' '}
<span className="font-medium">{pagination.to}</span> of{' '}
<span className="font-medium">{pagination.total}</span> results
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px">
<button
onClick={() => setPage(page - 1)}
disabled={page <= 1}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
Previous
</button>
<span className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
{page} of {pagination.last_page}
</span>
<button
onClick={() => setPage(page + 1)}
disabled={page >= pagination.last_page}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
Next
</button>
</nav>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default Articles;

View file

@ -1,191 +0,0 @@
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { FileText, Rss, Users, Route, TrendingUp, Clock, CheckCircle } from 'lucide-react';
import { apiClient } from '../lib/api';
const Dashboard: React.FC = () => {
const { data: stats, isLoading, error } = useQuery({
queryKey: ['dashboard-stats'],
queryFn: () => apiClient.getDashboardStats(),
});
if (isLoading) {
return (
<div className="p-6">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
{[...Array(4)].map((_, i) => (
<div key={i} className="bg-white p-6 rounded-lg shadow">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-8 bg-gray-200 rounded w-1/2"></div>
</div>
))}
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="p-6">
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<p className="text-red-600">Failed to load dashboard data</p>
</div>
</div>
);
}
const articleStats = stats?.article_stats;
const systemStats = stats?.system_stats;
return (
<div className="p-6">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Dashboard</h1>
<p className="mt-1 text-sm text-gray-500">
Overview of your feed management system
</p>
</div>
{/* Article Statistics */}
<div className="mb-8">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Article Statistics</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-shrink-0">
<FileText className="h-8 w-8 text-blue-500" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Articles Today</p>
<p className="text-2xl font-semibold text-gray-900">
{articleStats?.total_today || 0}
</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-shrink-0">
<Clock className="h-8 w-8 text-yellow-500" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Articles This Week</p>
<p className="text-2xl font-semibold text-gray-900">
{articleStats?.total_week || 0}
</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-shrink-0">
<CheckCircle className="h-8 w-8 text-green-500" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Approved Today</p>
<p className="text-2xl font-semibold text-gray-900">
{articleStats?.approved_today || 0}
</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-shrink-0">
<TrendingUp className="h-8 w-8 text-purple-500" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Approval Rate</p>
<p className="text-2xl font-semibold text-gray-900">
{articleStats?.approval_percentage_today?.toFixed(1) || 0}%
</p>
</div>
</div>
</div>
</div>
</div>
{/* System Statistics */}
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-4">System Overview</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-shrink-0">
<Rss className="h-8 w-8 text-orange-500" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Active Feeds</p>
<p className="text-2xl font-semibold text-gray-900">
{systemStats?.active_feeds || 0}
<span className="text-sm font-normal text-gray-500">
/{systemStats?.total_feeds || 0}
</span>
</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-shrink-0">
<Users className="h-8 w-8 text-indigo-500" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Platform Accounts</p>
<p className="text-2xl font-semibold text-gray-900">
{systemStats?.active_platform_accounts || 0}
<span className="text-sm font-normal text-gray-500">
/{systemStats?.total_platform_accounts || 0}
</span>
</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-shrink-0">
<FileText className="h-8 w-8 text-cyan-500" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Platform Channels</p>
<p className="text-2xl font-semibold text-gray-900">
{systemStats?.active_platform_channels || 0}
<span className="text-sm font-normal text-gray-500">
/{systemStats?.total_platform_channels || 0}
</span>
</p>
</div>
</div>
</div>
<div className="bg-white p-6 rounded-lg shadow">
<div className="flex items-center">
<div className="flex-shrink-0">
<Route className="h-8 w-8 text-pink-500" />
</div>
<div className="ml-4">
<p className="text-sm font-medium text-gray-500">Active Routes</p>
<p className="text-2xl font-semibold text-gray-900">
{systemStats?.active_routes || 0}
<span className="text-sm font-normal text-gray-500">
/{systemStats?.total_routes || 0}
</span>
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Dashboard;

View file

@ -1,145 +0,0 @@
import React from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Rss, Globe, ToggleLeft, ToggleRight, ExternalLink } from 'lucide-react';
import { apiClient, Feed } from '../lib/api';
const Feeds: React.FC = () => {
const queryClient = useQueryClient();
const { data: feeds, isLoading, error } = useQuery({
queryKey: ['feeds'],
queryFn: () => apiClient.getFeeds(),
});
const toggleMutation = useMutation({
mutationFn: (feedId: number) => apiClient.toggleFeed(feedId),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['feeds'] });
},
});
const handleToggle = (feedId: number) => {
toggleMutation.mutate(feedId);
};
const getTypeIcon = (type: string) => {
switch (type) {
case 'rss':
return <Rss className="h-5 w-5 text-orange-500" />;
case 'website':
return <Globe className="h-5 w-5 text-blue-500" />;
default:
return <Rss className="h-5 w-5 text-gray-500" />;
}
};
if (isLoading) {
return (
<div className="p-6">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<div key={i} className="bg-white p-6 rounded-lg shadow">
<div className="h-4 bg-gray-200 rounded w-3/4 mb-2"></div>
<div className="h-3 bg-gray-200 rounded w-1/2 mb-4"></div>
<div className="h-8 bg-gray-200 rounded w-20"></div>
</div>
))}
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="p-6">
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<p className="text-red-600">Failed to load feeds</p>
</div>
</div>
);
}
return (
<div className="p-6">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Feeds</h1>
<p className="mt-1 text-sm text-gray-500">
Manage your RSS feeds and website sources
</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{feeds?.map((feed: Feed) => (
<div key={feed.id} className="bg-white rounded-lg shadow p-6">
<div className="flex items-start justify-between mb-4">
<div className="flex items-center">
{getTypeIcon(feed.type)}
<h3 className="ml-2 text-lg font-medium text-gray-900 truncate">
{feed.name}
</h3>
</div>
<button
onClick={() => handleToggle(feed.id)}
disabled={toggleMutation.isPending}
className="flex-shrink-0"
>
{feed.is_active ? (
<ToggleRight className="h-6 w-6 text-green-500" />
) : (
<ToggleLeft className="h-6 w-6 text-gray-300" />
)}
</button>
</div>
<p className="text-sm text-gray-600 mb-4 line-clamp-2">
{feed.description || 'No description provided'}
</p>
<div className="flex items-center justify-between">
<div className="flex items-center space-x-2">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
feed.is_active
? 'bg-green-100 text-green-800'
: 'bg-gray-100 text-gray-800'
}`}>
{feed.is_active ? 'Active' : 'Inactive'}
</span>
<span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{feed.type.toUpperCase()}
</span>
</div>
<a
href={feed.url}
target="_blank"
rel="noopener noreferrer"
className="p-1 text-gray-400 hover:text-gray-600 rounded"
title="Visit feed URL"
>
<ExternalLink className="h-4 w-4" />
</a>
</div>
<div className="mt-4 text-xs text-gray-500">
Added {new Date(feed.created_at).toLocaleDateString()}
</div>
</div>
))}
{feeds?.length === 0 && (
<div className="col-span-full text-center py-12">
<Rss className="mx-auto h-12 w-12 text-gray-400" />
<h3 className="mt-2 text-sm font-medium text-gray-900">No feeds</h3>
<p className="mt-1 text-sm text-gray-500">
Get started by adding your first feed.
</p>
</div>
)}
</div>
</div>
);
};
export default Feeds;

View file

@ -1,129 +0,0 @@
import React, { useState } from 'react';
import { Eye, EyeOff, LogIn } from 'lucide-react';
import { apiClient } from '../lib/api';
const Login: React.FC = () => {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [showPassword, setShowPassword] = useState(false);
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
const authData = await apiClient.login({ email, password });
apiClient.setAuth(authData.token, authData.user);
window.location.reload();
} catch (err: any) {
setError(err.response?.data?.message || 'Login failed. Please try again.');
} finally {
setLoading(false);
}
};
return (
<div className="min-h-screen bg-gray-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<div className="flex justify-center">
<img className="h-12 w-auto" src="/images/ffr-logo-600.png" alt="FFR" />
</div>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-gray-900">
Sign in to FFR
</h2>
<p className="mt-2 text-center text-sm text-gray-600">
Feed Feed Reader - Article Management System
</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow sm:rounded-lg sm:px-10">
<form className="space-y-6" onSubmit={handleSubmit}>
{error && (
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<p className="text-sm text-red-600">{error}</p>
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
Email address
</label>
<div className="mt-1">
<input
id="email"
name="email"
type="email"
autoComplete="email"
required
value={email}
onChange={(e) => setEmail(e.target.value)}
className="appearance-none block w-full px-3 py-2 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
placeholder="Enter your email"
/>
</div>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-gray-700">
Password
</label>
<div className="mt-1 relative">
<input
id="password"
name="password"
type={showPassword ? 'text' : 'password'}
autoComplete="current-password"
required
value={password}
onChange={(e) => setPassword(e.target.value)}
className="appearance-none block w-full px-3 py-2 pr-10 border border-gray-300 rounded-md placeholder-gray-400 focus:outline-none focus:ring-blue-500 focus:border-blue-500 sm:text-sm"
placeholder="Enter your password"
/>
<button
type="button"
className="absolute inset-y-0 right-0 pr-3 flex items-center"
onClick={() => setShowPassword(!showPassword)}
>
{showPassword ? (
<EyeOff className="h-4 w-4 text-gray-400" />
) : (
<Eye className="h-4 w-4 text-gray-400" />
)}
</button>
</div>
</div>
<div>
<button
type="submit"
disabled={loading}
className="w-full flex justify-center items-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
>
{loading ? (
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white" />
) : (
<>
<LogIn className="h-4 w-4 mr-2" />
Sign in
</>
)}
</button>
</div>
</form>
<div className="mt-6">
<div className="text-center text-xs text-gray-500">
Demo credentials: test@example.com / password123
</div>
</div>
</div>
</div>
</div>
);
};
export default Login;

View file

@ -1,148 +0,0 @@
import React from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Settings as SettingsIcon, Save, ToggleLeft, ToggleRight } from 'lucide-react';
import { apiClient } from '../lib/api';
const Settings: React.FC = () => {
const queryClient = useQueryClient();
const { data: settings, isLoading, error } = useQuery({
queryKey: ['settings'],
queryFn: () => apiClient.getSettings(),
});
const updateMutation = useMutation({
mutationFn: (data: any) => apiClient.updateSettings(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['settings'] });
},
});
const handleToggle = (key: string, value: boolean) => {
updateMutation.mutate({ [key]: value });
};
if (isLoading) {
return (
<div className="p-6">
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-1/4 mb-6"></div>
<div className="space-y-6">
<div className="bg-white p-6 rounded-lg shadow">
<div className="h-4 bg-gray-200 rounded w-1/3 mb-4"></div>
<div className="space-y-4">
<div className="h-12 bg-gray-200 rounded"></div>
<div className="h-12 bg-gray-200 rounded"></div>
</div>
</div>
</div>
</div>
</div>
);
}
if (error) {
return (
<div className="p-6">
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<p className="text-red-600">Failed to load settings</p>
</div>
</div>
);
}
return (
<div className="p-6">
<div className="mb-8">
<h1 className="text-2xl font-bold text-gray-900">Settings</h1>
<p className="mt-1 text-sm text-gray-500">
Configure your system preferences
</p>
</div>
<div className="space-y-6">
{/* Article Processing Settings */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="text-lg font-medium text-gray-900 flex items-center">
<SettingsIcon className="h-5 w-5 mr-2" />
Article Processing
</h2>
<p className="mt-1 text-sm text-gray-500">
Control how articles are processed and handled
</p>
</div>
<div className="px-6 py-4 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-900">
Article Processing Enabled
</h3>
<p className="text-sm text-gray-500">
Enable automatic fetching and processing of articles from feeds
</p>
</div>
<button
onClick={() => handleToggle('article_processing_enabled', !settings?.article_processing_enabled)}
disabled={updateMutation.isPending}
className="flex-shrink-0"
>
{settings?.article_processing_enabled ? (
<ToggleRight className="h-6 w-6 text-green-500" />
) : (
<ToggleLeft className="h-6 w-6 text-gray-300" />
)}
</button>
</div>
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-gray-900">
Publishing Approvals Required
</h3>
<p className="text-sm text-gray-500">
Require manual approval before articles are published to platforms
</p>
</div>
<button
onClick={() => handleToggle('enable_publishing_approvals', !settings?.publishing_approvals_enabled)}
disabled={updateMutation.isPending}
className="flex-shrink-0"
>
{settings?.publishing_approvals_enabled ? (
<ToggleRight className="h-6 w-6 text-green-500" />
) : (
<ToggleLeft className="h-6 w-6 text-gray-300" />
)}
</button>
</div>
</div>
</div>
{/* Status indicator */}
{updateMutation.isPending && (
<div className="bg-blue-50 border border-blue-200 rounded-md p-4">
<div className="flex items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-600 mr-2"></div>
<p className="text-blue-600 text-sm">Updating settings...</p>
</div>
</div>
)}
{updateMutation.isError && (
<div className="bg-red-50 border border-red-200 rounded-md p-4">
<p className="text-red-600 text-sm">Failed to update settings. Please try again.</p>
</div>
)}
{updateMutation.isSuccess && (
<div className="bg-green-50 border border-green-200 rounded-md p-4">
<p className="text-green-600 text-sm">Settings updated successfully!</p>
</div>
)}
</div>
</div>
);
};
export default Settings;

View file

@ -1,40 +0,0 @@
/**
* Routing page JavaScript functionality
* Handles dynamic keyword input management
*/
function addKeyword() {
const container = document.getElementById('keywords-container');
const newGroup = document.createElement('div');
newGroup.className = 'keyword-input-group flex items-center space-x-2';
newGroup.innerHTML = `
<input type="text"
name="keywords[]"
class="flex-1 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Enter keyword">
<button type="button"
class="remove-keyword bg-red-500 hover:bg-red-600 text-white px-3 py-2 rounded-md text-sm"
onclick="removeKeyword(this)">
Remove
</button>
`;
container.appendChild(newGroup);
}
function removeKeyword(button) {
const container = document.getElementById('keywords-container');
const groups = container.querySelectorAll('.keyword-input-group');
// Don't remove if it's the last remaining input
if (groups.length > 1) {
button.parentElement.remove();
} else {
// Clear the input value instead of removing the field
const input = button.parentElement.querySelector('input');
input.value = '';
}
}
// Make functions globally available for onclick handlers
window.addKeyword = addKeyword;
window.removeKeyword = removeKeyword;

View file

@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'FFR') }}</title>
<!-- Favicon -->
<link rel="icon" type="image/svg+xml" href="/vite.svg">
<!-- Built React CSS -->
<link rel="stylesheet" href="/assets/index-BVZJGwOp.css">
</head>
<body>
<div id="root"></div>
<!-- Built React JS -->
<script type="module" src="/assets/index-CLe1lYXi.js"></script>
</body>
</html>

View file

@ -1,30 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'Lemmy Poster Admin')</title>
<script src="https://cdn.tailwindcss.com"></script>
@vite(['resources/css/app.css', 'resources/js/app.js'])
</head>
<body class="bg-gray-100">
<div class="flex h-screen">
@include('partials.sidebar')
<!-- Main Content -->
<div class="flex-1 flex flex-col overflow-hidden">
<!-- Header -->
<header class="bg-white shadow-sm border-b border-gray-200">
<div class="px-6 py-4">
<h1 class="text-2xl font-semibold text-gray-800">@yield('page-title', 'Dashboard')</h1>
</div>
</header>
<!-- Content -->
<main class="flex-1 overflow-y-auto p-6">
@yield('content')
</main>
</div>
</div>
</body>
</html>

View file

@ -1,110 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="min-h-screen flex items-center justify-center bg-gray-50">
<div class="max-w-lg w-full bg-white rounded-lg shadow-md p-8">
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-gray-900 mb-2">Configure Your Channel</h1>
<p class="text-gray-600">
Set up a Lemmy community where articles will be posted
</p>
<!-- Progress indicator -->
<div class="flex justify-center mt-6 space-x-2">
<div class="w-6 h-6 bg-green-500 text-white rounded-full flex items-center justify-center text-xs font-semibold"></div>
<div class="w-6 h-6 bg-green-500 text-white rounded-full flex items-center justify-center text-xs font-semibold"></div>
<div class="w-6 h-6 bg-blue-500 text-white rounded-full flex items-center justify-center text-xs font-semibold">3</div>
<div class="w-6 h-6 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center text-xs font-semibold">4</div>
</div>
</div>
<form action="{{ route('channels.store') }}" method="POST" class="space-y-6">
@csrf
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
Community Name
</label>
<input type="text"
id="name"
name="name"
value="{{ old('name') }}"
placeholder="technology"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required>
<p class="text-sm text-gray-500 mt-1">Enter the community name (without the @ or instance)</p>
@error('name')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="platform_instance_id" class="block text-sm font-medium text-gray-700 mb-2">
Platform Instance
</label>
<select id="platform_instance_id"
name="platform_instance_id"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required>
@foreach(\App\Models\PlatformInstance::where('is_active', true)->get() as $instance)
<option value="{{ $instance->id }}" {{ old('platform_instance_id') == $instance->id ? 'selected' : '' }}>
{{ $instance->name }} ({{ $instance->url }})
</option>
@endforeach
</select>
@error('platform_instance_id')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="language_id" class="block text-sm font-medium text-gray-700 mb-2">
Language
</label>
<select id="language_id"
name="language_id"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required>
<option value="">Select language</option>
@foreach(\App\Models\Language::orderBy('name')->get() as $language)
<option value="{{ $language->id }}" {{ old('language_id') == $language->id ? 'selected' : '' }}>
{{ $language->name }}
</option>
@endforeach
</select>
@error('language_id')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
Description (Optional)
</label>
<textarea id="description"
name="description"
rows="3"
placeholder="Brief description of this channel"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">{{ old('description') }}</textarea>
@error('description')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<input type="hidden" name="is_active" value="1">
<input type="hidden" name="redirect_to" value="{{ route('onboarding.complete') }}">
<div class="flex justify-between">
<a href="{{ route('onboarding.feed') }}"
class="px-4 py-2 text-gray-600 hover:text-gray-800 transition duration-200">
Back
</a>
<button type="submit"
class="bg-blue-600 text-white py-2 px-6 rounded-md hover:bg-blue-700 transition duration-200">
Continue
</button>
</div>
</form>
</div>
</div>
@endsection

View file

@ -1,121 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="min-h-screen flex items-center justify-center bg-gray-50">
<div class="max-w-lg w-full bg-white rounded-lg shadow-md p-8">
<div class="text-center">
<div class="mb-6">
<div class="w-16 h-16 bg-green-500 text-white rounded-full flex items-center justify-center mx-auto mb-4">
<svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
</svg>
</div>
<h1 class="text-3xl font-bold text-gray-900 mb-2">Setup Complete!</h1>
<p class="text-gray-600 mb-6">
Great! You've successfully configured Lemmy Poster. Now activate the system to start monitoring feeds and posting articles.
</p>
</div>
<!-- Progress indicator -->
<div class="flex justify-center mb-8 space-x-2">
<div class="w-6 h-6 bg-green-500 text-white rounded-full flex items-center justify-center text-xs font-semibold"></div>
<div class="w-6 h-6 bg-green-500 text-white rounded-full flex items-center justify-center text-xs font-semibold"></div>
<div class="w-6 h-6 bg-green-500 text-white rounded-full flex items-center justify-center text-xs font-semibold"></div>
<div class="w-6 h-6 bg-green-500 text-white rounded-full flex items-center justify-center text-xs font-semibold"></div>
</div>
<!-- System Activation Section -->
<div class="mb-8">
<div class="bg-white border-2 border-gray-200 rounded-lg p-6">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
@if($systemStatus['is_enabled'])
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center mr-3">
<x-heroicon-o-check-circle class="w-5 h-5 text-green-600" />
</div>
<div>
<h3 class="font-semibold text-gray-900">System Status</h3>
<p class="text-sm {{ $systemStatus['status_class'] }}">{{ $systemStatus['status'] }}</p>
</div>
@else
<div class="w-8 h-8 bg-orange-100 rounded-full flex items-center justify-center mr-3">
<x-heroicon-o-exclamation-triangle class="w-5 h-5 text-orange-600" />
</div>
<div>
<h3 class="font-semibold text-gray-900">Ready to Activate</h3>
<p class="text-sm text-orange-600">System is configured but not active</p>
</div>
@endif
</div>
@if(!$systemStatus['is_enabled'])
<form action="{{ route('settings.update') }}" method="POST" class="inline">
@csrf
@method('PUT')
<input type="hidden" name="article_processing_enabled" value="1">
<input type="hidden" name="from" value="onboarding">
<button type="submit" class="bg-green-600 hover:bg-green-700 text-white px-4 py-2 rounded-md font-medium transition-colors">
Activate System
</button>
</form>
@else
<span class="text-green-600 font-medium"> Active</span>
@endif
</div>
@if(!$systemStatus['is_enabled'] && count($systemStatus['reasons']) > 0)
<div class="mt-4 pt-4 border-t border-gray-200">
<p class="text-sm text-gray-600 mb-2">System will be enabled once activated. Current setup status:</p>
<ul class="text-sm text-gray-600 space-y-1">
@foreach($systemStatus['reasons'] as $reason)
@if($reason !== 'Manually disabled by user')
<li class="flex items-center">
<x-heroicon-o-check-circle class="w-4 h-4 text-green-500 mr-2 flex-shrink-0" />
{{ str_replace('No active', 'Active', $reason) . ' ✓' }}
</li>
@endif
@endforeach
</ul>
</div>
@endif
</div>
</div>
<div class="space-y-4 mb-8">
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 class="font-semibold text-blue-900 mb-2">What happens next?</h3>
<ul class="text-sm text-blue-800 space-y-1 text-left">
<li> Your feeds will be checked regularly for new articles</li>
<li> New articles will be automatically posted to your channels</li>
<li> You can monitor activity in the Articles and Logs sections</li>
</ul>
</div>
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-4">
<h3 class="font-semibold text-yellow-900 mb-2">Want more control?</h3>
<p class="text-sm text-yellow-800 text-left mb-2">
Set up <strong>routing rules</strong> to control which articles get posted where based on keywords, titles, or content.
</p>
<a href="{{ route('routing.index') }}"
class="inline-block bg-yellow-200 hover:bg-yellow-300 text-yellow-900 px-3 py-1 rounded-md text-sm transition duration-200">
Configure Routing
</a>
</div>
</div>
<div class="space-y-3">
<a href="{{ route('feeds.index') }}"
class="w-full bg-blue-600 text-white py-3 px-4 rounded-md hover:bg-blue-700 transition duration-200 inline-block">
Go to Dashboard
</a>
<div class="text-sm text-gray-500">
<a href="{{ route('articles') }}" class="hover:text-blue-600">View Articles</a>
<a href="{{ route('logs') }}" class="hover:text-blue-600">Check Logs</a>
</div>
</div>
</div>
</div>
</div>
@endsection

View file

@ -1,123 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="min-h-screen flex items-center justify-center bg-gray-50">
<div class="max-w-lg w-full bg-white rounded-lg shadow-md p-8">
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-gray-900 mb-2">Add Your First Feed</h1>
<p class="text-gray-600">
Add a RSS feed or website to monitor for new articles
</p>
<!-- Progress indicator -->
<div class="flex justify-center mt-6 space-x-2">
<div class="w-6 h-6 bg-green-500 text-white rounded-full flex items-center justify-center text-xs font-semibold"></div>
<div class="w-6 h-6 bg-blue-500 text-white rounded-full flex items-center justify-center text-xs font-semibold">2</div>
<div class="w-6 h-6 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center text-xs font-semibold">3</div>
<div class="w-6 h-6 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center text-xs font-semibold">4</div>
</div>
</div>
<form action="{{ route('feeds.store') }}" method="POST" class="space-y-6">
@csrf
<div>
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">
Feed Name
</label>
<input type="text"
id="name"
name="name"
value="{{ old('name') }}"
placeholder="My News Feed"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required>
@error('name')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="url" class="block text-sm font-medium text-gray-700 mb-2">
Feed URL
</label>
<input type="url"
id="url"
name="url"
value="{{ old('url') }}"
placeholder="https://example.com/rss.xml"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required>
@error('url')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="type" class="block text-sm font-medium text-gray-700 mb-2">
Feed Type
</label>
<select id="type"
name="type"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required>
<option value="">Select feed type</option>
<option value="rss" {{ old('type') == 'rss' ? 'selected' : '' }}>RSS Feed</option>
<option value="website" {{ old('type') == 'website' ? 'selected' : '' }}>Website</option>
</select>
@error('type')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="language_id" class="block text-sm font-medium text-gray-700 mb-2">
Language
</label>
<select id="language_id"
name="language_id"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required>
<option value="">Select language</option>
@foreach(\App\Models\Language::orderBy('name')->get() as $language)
<option value="{{ $language->id }}" {{ old('language_id') == $language->id ? 'selected' : '' }}>
{{ $language->name }}
</option>
@endforeach
</select>
@error('language_id')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">
Description (Optional)
</label>
<textarea id="description"
name="description"
rows="3"
placeholder="Brief description of this feed"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">{{ old('description') }}</textarea>
@error('description')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<input type="hidden" name="is_active" value="1">
<input type="hidden" name="redirect_to" value="{{ route('onboarding.channel') }}">
<div class="flex justify-between">
<a href="{{ route('onboarding.platform') }}"
class="px-4 py-2 text-gray-600 hover:text-gray-800 transition duration-200">
Back
</a>
<button type="submit"
class="bg-blue-600 text-white py-2 px-6 rounded-md hover:bg-blue-700 transition duration-200">
Continue
</button>
</div>
</form>
</div>
</div>
@endsection

View file

@ -1,86 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="min-h-screen flex items-center justify-center bg-gray-50">
<div class="max-w-lg w-full bg-white rounded-lg shadow-md p-8">
<div class="text-center mb-8">
<h1 class="text-2xl font-bold text-gray-900 mb-2">Connect Your Lemmy Account</h1>
<p class="text-gray-600">
Enter your Lemmy instance details and login credentials
</p>
<!-- Progress indicator -->
<div class="flex justify-center mt-6 space-x-2">
<div class="w-6 h-6 bg-blue-500 text-white rounded-full flex items-center justify-center text-xs font-semibold">1</div>
<div class="w-6 h-6 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center text-xs font-semibold">2</div>
<div class="w-6 h-6 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center text-xs font-semibold">3</div>
<div class="w-6 h-6 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center text-xs font-semibold">4</div>
</div>
</div>
<form action="{{ route('platforms.store') }}" method="POST" class="space-y-6">
@csrf
<div>
<label for="instance_url" class="block text-sm font-medium text-gray-700 mb-2">
Lemmy Instance URL
</label>
<input type="url"
id="instance_url"
name="instance_url"
value="{{ old('instance_url') }}"
placeholder="https://lemmy.world"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required>
@error('instance_url')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="username" class="block text-sm font-medium text-gray-700 mb-2">
Username
</label>
<input type="text"
id="username"
name="username"
value="{{ old('username') }}"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required>
@error('username')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<div>
<label for="password" class="block text-sm font-medium text-gray-700 mb-2">
Password
</label>
<input type="password"
id="password"
name="password"
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
required>
@error('password')
<p class="text-red-600 text-sm mt-1">{{ $message }}</p>
@enderror
</div>
<input type="hidden" name="platform" value="lemmy">
<input type="hidden" name="is_active" value="1">
<input type="hidden" name="redirect_to" value="{{ route('onboarding.feed') }}">
<div class="flex justify-between">
<a href="{{ route('onboarding.index') }}"
class="px-4 py-2 text-gray-600 hover:text-gray-800 transition duration-200">
Back
</a>
<button type="submit"
class="bg-blue-600 text-white py-2 px-6 rounded-md hover:bg-blue-700 transition duration-200">
Continue
</button>
</div>
</form>
</div>
</div>
@endsection

View file

@ -1,40 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="min-h-screen flex items-center justify-center bg-gray-50">
<div class="max-w-md w-full bg-white rounded-lg shadow-md p-8">
<div class="text-center">
<h1 class="text-3xl font-bold text-gray-900 mb-2">Welcome to Lemmy Poster</h1>
<p class="text-gray-600 mb-8">
Let's get you set up! We'll help you configure your Lemmy account, add your first feed, and create a channel for posting.
</p>
<div class="space-y-4">
<div class="flex items-center text-sm text-gray-600">
<div class="w-6 h-6 bg-blue-500 text-white rounded-full flex items-center justify-center mr-3 text-xs font-semibold">1</div>
<span>Connect your Lemmy account</span>
</div>
<div class="flex items-center text-sm text-gray-600">
<div class="w-6 h-6 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center mr-3 text-xs font-semibold">2</div>
<span>Add your first feed</span>
</div>
<div class="flex items-center text-sm text-gray-600">
<div class="w-6 h-6 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center mr-3 text-xs font-semibold">3</div>
<span>Configure a channel</span>
</div>
<div class="flex items-center text-sm text-gray-600">
<div class="w-6 h-6 bg-gray-300 text-gray-600 rounded-full flex items-center justify-center mr-3 text-xs font-semibold">4</div>
<span>You're ready to go!</span>
</div>
</div>
<div class="mt-8">
<a href="{{ route('onboarding.platform') }}"
class="w-full bg-blue-600 text-white py-3 px-4 rounded-md hover:bg-blue-700 transition duration-200 inline-block">
Get Started
</a>
</div>
</div>
</div>
</div>
@endsection

View file

@ -1,79 +0,0 @@
@extends('layouts.app')
@section('page-title', 'Articles')
@section('content')
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Article Feed</h2>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">URL</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Approval</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created At</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach($articles as $article)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ $article->id }}</td>
<td class="px-6 py-4 text-sm text-gray-900">
<a href="{{ $article->url }}" target="_blank" class="text-blue-600 hover:text-blue-900 hover:underline">
{{ Str::limit($article->url, 60) }}
</a>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full
{{ $article->articlePublication ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800' }}">
{{ $article->articlePublication ? 'Published' : 'Pending' }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full
@if($article->approval_status === 'approved') bg-green-100 text-green-800
@elseif($article->approval_status === 'rejected') bg-red-100 text-red-800
@else bg-yellow-100 text-yellow-800 @endif">
{{ ucfirst($article->approval_status) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $article->created_at->format('Y-m-d H:i') }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
@if($publishingApprovalsEnabled && $article->isValid() && $article->isPending() && !$article->articlePublication)
<div class="flex space-x-2">
<form method="POST" action="{{ route('articles.approve', $article) }}" class="inline">
@csrf
<button type="submit" class="text-green-600 hover:text-green-900 font-medium">
Approve & Publish
</button>
</form>
<form method="POST" action="{{ route('articles.reject', $article) }}" class="inline">
@csrf
<button type="submit" class="text-red-600 hover:text-red-900 font-medium">
Reject
</button>
</form>
</div>
@elseif($article->isValid() && !$article->articlePublication)
<span class="text-gray-500">Auto-publishing enabled</span>
@endif
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@if($articles->hasPages())
<div class="px-6 py-4 border-t border-gray-200">
{{ $articles->links() }}
</div>
@endif
</div>
@endsection

View file

@ -1,100 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="max-w-2xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="mb-6">
<h1 class="text-2xl font-semibold text-gray-900">Create New Channel</h1>
<p class="mt-1 text-sm text-gray-600">Add a new publishing channel to your platform</p>
</div>
<div class="bg-white shadow sm:rounded-lg">
<form action="{{ route('channels.store') }}" method="POST" class="px-4 py-5 sm:p-6">
@csrf
<div class="grid grid-cols-1 gap-6">
<div>
<label for="platform_instance_id" class="block text-sm font-medium text-gray-700">Platform</label>
<select name="platform_instance_id" id="platform_instance_id" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('platform_instance_id') border-red-300 @enderror">
<option value="">Select a platform</option>
@foreach($instances as $instance)
<option value="{{ $instance->id }}" {{ old('platform_instance_id') == $instance->id ? 'selected' : '' }}>
{{ $instance->name }}
</option>
@endforeach
</select>
@error('platform_instance_id')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Channel Name</label>
<input type="text" name="name" id="name" required
value="{{ old('name') }}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('name') border-red-300 @enderror"
placeholder="technology">
<p class="mt-1 text-sm text-gray-500">The channel identifier (e.g., "technology", "news")</p>
@error('name')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="display_name" class="block text-sm font-medium text-gray-700">Display Name</label>
<input type="text" name="display_name" id="display_name"
value="{{ old('display_name') }}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('display_name') border-red-300 @enderror"
placeholder="Technology">
<p class="mt-1 text-sm text-gray-500">Human-readable name for the channel (optional)</p>
@error('display_name')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="channel_id" class="block text-sm font-medium text-gray-700">Channel ID</label>
<input type="text" name="channel_id" id="channel_id"
value="{{ old('channel_id') }}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('channel_id') border-red-300 @enderror"
placeholder="12345">
<p class="mt-1 text-sm text-gray-500">Platform-specific channel ID (optional)</p>
@error('channel_id')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
<textarea name="description" id="description" rows="3"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('description') border-red-300 @enderror"
placeholder="Channel for technology-related content">{{ old('description') }}</textarea>
@error('description')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="flex items-center">
<input type="checkbox" name="is_active" id="is_active" value="1"
{{ old('is_active', true) ? 'checked' : '' }}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="is_active" class="ml-2 block text-sm text-gray-900">
Active (channel can receive published content)
</label>
</div>
</div>
<div class="mt-6 flex items-center justify-end space-x-3">
<a href="{{ route('channels.index') }}" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">
Cancel
</a>
<button type="submit" class="bg-indigo-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-indigo-700">
Create Channel
</button>
</div>
</form>
</div>
</div>
</div>
@endsection

View file

@ -1,104 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="max-w-2xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="mb-6">
<h1 class="text-2xl font-semibold text-gray-900">Edit Channel</h1>
<p class="mt-1 text-sm text-gray-600">Update channel details</p>
</div>
<div class="bg-white shadow sm:rounded-lg">
<form action="{{ route('channels.update', $channel) }}" method="POST" class="px-4 py-5 sm:p-6">
@csrf
@method('PUT')
<div class="grid grid-cols-1 gap-6">
<div>
<label for="platform_instance_id" class="block text-sm font-medium text-gray-700">Platform</label>
<select name="platform_instance_id" id="platform_instance_id" required
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('platform_instance_id') border-red-300 @enderror">
@foreach($instances as $instance)
<option value="{{ $instance->id }}" {{ old('platform_instance_id', $channel->platform_instance_id) == $instance->id ? 'selected' : '' }}>
{{ $instance->name }}
</option>
@endforeach
</select>
@error('platform_instance_id')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Channel Name</label>
<input type="text" name="name" id="name" required
value="{{ old('name', $channel->name) }}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('name') border-red-300 @enderror">
@error('name')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="display_name" class="block text-sm font-medium text-gray-700">Display Name</label>
<input type="text" name="display_name" id="display_name"
value="{{ old('display_name', $channel->display_name) }}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('display_name') border-red-300 @enderror">
@error('display_name')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="channel_id" class="block text-sm font-medium text-gray-700">Channel ID</label>
<input type="text" name="channel_id" id="channel_id"
value="{{ old('channel_id', $channel->channel_id) }}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('channel_id') border-red-300 @enderror">
@error('channel_id')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
<textarea name="description" id="description" rows="3"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('description') border-red-300 @enderror">{{ old('description', $channel->description) }}</textarea>
@error('description')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="flex items-center">
<input type="checkbox" name="is_active" id="is_active" value="1"
{{ old('is_active', $channel->is_active) ? 'checked' : '' }}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="is_active" class="ml-2 block text-sm text-gray-900">
Active (channel can receive published content)
</label>
</div>
</div>
<div class="mt-6 flex items-center justify-between">
<form action="{{ route('channels.destroy', $channel) }}" method="POST" class="inline"
onsubmit="return confirm('Are you sure you want to delete this channel?')">
@csrf
@method('DELETE')
<button type="submit" class="bg-red-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-red-700">
Delete Channel
</button>
</form>
<div class="flex items-center space-x-3">
<a href="{{ route('channels.show', $channel) }}" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">
Cancel
</a>
<button type="submit" class="bg-indigo-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-indigo-700">
Update Channel
</button>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection

View file

@ -1,99 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="flex justify-between items-center mb-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Platform Channels</h1>
<p class="mt-1 text-sm text-gray-600">Manage channels for publishing content</p>
</div>
<a href="{{ route('channels.create') }}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Add New Channel
</a>
</div>
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
{{ session('success') }}
</div>
@endif
<div class="bg-white shadow overflow-hidden sm:rounded-md">
@if($channels->count() > 0)
<ul class="divide-y divide-gray-200">
@foreach($channels as $channel)
<li>
<div class="px-4 py-4 flex items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0">
<x-heroicon-o-hashtag class="w-5 h-5 text-gray-400" />
</div>
<div class="ml-4">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900">{{ $channel->display_name ?? $channel->name }}</div>
@if(!$channel->is_active)
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
Inactive
</span>
@else
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
Active
</span>
@endif
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{{ $channel->platformInstance->name }}
</span>
</div>
<div class="text-sm text-gray-500">{{ $channel->name }}</div>
@if($channel->description)
<div class="text-sm text-gray-500 mt-1">{{ Str::limit($channel->description, 100) }}</div>
@endif
</div>
</div>
<div class="flex items-center space-x-3">
<!-- Toggle Switch -->
<form action="{{ route('channels.toggle', $channel) }}" method="POST" class="inline">
@csrf
<label class="inline-flex items-center cursor-pointer">
<span class="text-xs text-gray-600 mr-2">{{ $channel->is_active ? 'Active' : 'Inactive' }}</span>
<button type="submit" class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors {{ $channel->is_active ? 'bg-green-600' : 'bg-gray-200' }}">
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform {{ $channel->is_active ? 'translate-x-6' : 'translate-x-1' }}"></span>
</button>
</label>
</form>
<!-- Action Buttons -->
<a href="{{ route('channels.show', $channel) }}" class="text-indigo-600 hover:text-indigo-900 text-sm font-medium">
View
</a>
<a href="{{ route('channels.edit', $channel) }}" class="text-indigo-600 hover:text-indigo-900 text-sm font-medium">
Edit
</a>
<form action="{{ route('channels.destroy', $channel) }}" method="POST" class="inline"
onsubmit="return confirm('Are you sure you want to delete this channel?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-900 text-sm font-medium">
Delete
</button>
</form>
</div>
</div>
</li>
@endforeach
</ul>
@else
<div class="text-center py-12">
<x-heroicon-o-hashtag class="w-24 h-24 text-gray-400 mb-4" />
<h3 class="text-lg font-medium text-gray-900 mb-2">No channels yet</h3>
<p class="text-gray-500 mb-4">Get started by adding your first publishing channel.</p>
<a href="{{ route('channels.create') }}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Add New Channel
</a>
</div>
@endif
</div>
</div>
</div>
@endsection

View file

@ -1,177 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="max-w-4xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<!-- Header -->
<div class="flex justify-between items-center mb-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">{{ $channel->display_name ?? $channel->name }}</h1>
<p class="mt-1 text-sm text-gray-600">{{ $channel->platformInstance->name }} Channel</p>
</div>
<div class="flex items-center space-x-3">
<a href="{{ route('channels.edit', $channel) }}" class="bg-indigo-600 hover:bg-indigo-700 text-white px-4 py-2 rounded-md">
Edit Channel
</a>
<a href="{{ route('channels.index') }}" class="bg-gray-600 hover:bg-gray-700 text-white px-4 py-2 rounded-md">
Back to Channels
</a>
</div>
</div>
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-6">
{{ session('success') }}
</div>
@endif
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Channel Status & Toggle -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center justify-between mb-4">
<div>
<h3 class="text-lg font-medium text-gray-900">Channel Status</h3>
<p class="text-sm text-gray-500">Enable or disable publishing to this channel</p>
</div>
@if($channel->is_active)
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<x-heroicon-o-check-circle class="w-5 h-5 text-green-600" />
</div>
@else
<div class="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center">
<x-heroicon-o-x-circle class="w-5 h-5 text-red-600" />
</div>
@endif
</div>
<div class="flex items-center justify-between">
<span class="text-lg font-medium {{ $channel->is_active ? 'text-green-600' : 'text-red-600' }}">
{{ $channel->is_active ? 'Active' : 'Inactive' }}
</span>
<form action="{{ route('channels.toggle', $channel) }}" method="POST">
@csrf
<button type="submit"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors {{ $channel->is_active ? 'bg-green-600' : 'bg-gray-200' }}"
title="{{ $channel->is_active ? 'Click to deactivate' : 'Click to activate' }}">
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform {{ $channel->is_active ? 'translate-x-6' : 'translate-x-1' }}"></span>
</button>
</form>
</div>
@if(!$channel->is_active)
<div class="mt-4 p-3 bg-yellow-50 border border-yellow-200 rounded-md">
<p class="text-sm text-yellow-800">
<x-heroicon-o-exclamation-triangle class="w-4 h-4 inline mr-1" />
This channel is inactive. No articles will be published here until reactivated.
</p>
</div>
@endif
</div>
</div>
<!-- Channel Details -->
<div class="lg:col-span-2 bg-white overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<h3 class="text-lg font-medium text-gray-900 mb-4">Channel Details</h3>
<dl class="grid grid-cols-1 gap-x-4 gap-y-4 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-gray-500">Channel Name</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $channel->name }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Display Name</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $channel->display_name ?? $channel->name }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Platform</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $channel->platformInstance->name }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Channel ID</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $channel->channel_id ?? 'Not set' }}</dd>
</div>
@if($channel->language)
<div>
<dt class="text-sm font-medium text-gray-500">Language</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $channel->language->name }}</dd>
</div>
@endif
<div>
<dt class="text-sm font-medium text-gray-500">Created</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $channel->created_at->format('M j, Y') }}</dd>
</div>
</dl>
@if($channel->description)
<div class="mt-4">
<dt class="text-sm font-medium text-gray-500">Description</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $channel->description }}</dd>
</div>
@endif
</div>
</div>
</div>
<!-- Connected Feeds -->
@if($channel->feeds->count() > 0)
<div class="mt-6 bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
<h3 class="text-lg leading-6 font-medium text-gray-900">Connected Feeds</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Feeds that route content to this channel</p>
</div>
<ul class="divide-y divide-gray-200">
@foreach($channel->feeds as $feed)
<li class="px-4 py-4">
<div class="flex items-center justify-between">
<div class="flex items-center">
@if($feed->type === 'rss')
<x-heroicon-o-rss class="w-4 h-4 text-orange-500 mr-3" />
@else
<x-heroicon-o-globe-alt class="w-4 h-4 text-blue-500 mr-3" />
@endif
<div>
<p class="text-sm font-medium text-gray-900">{{ $feed->name }}</p>
<p class="text-sm text-gray-500">{{ $feed->url }}</p>
</div>
@if(!$feed->pivot->is_active)
<span class="ml-3 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
Route Inactive
</span>
@endif
</div>
<div class="flex items-center space-x-2">
<a href="{{ route('feeds.show', $feed) }}" class="text-indigo-600 hover:text-indigo-900 text-sm">
View Feed
</a>
<a href="{{ route('routing.edit', [$feed, $channel]) }}" class="text-indigo-600 hover:text-indigo-900 text-sm">
Edit Route
</a>
</div>
</div>
</li>
@endforeach
</ul>
</div>
@else
<div class="mt-6 bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-8 text-center">
<x-heroicon-o-arrow-path class="w-12 h-12 text-gray-400 mx-auto mb-4" />
<h3 class="text-sm font-medium text-gray-900 mb-2">No feeds connected</h3>
<p class="text-sm text-gray-500 mb-4">This channel doesn't have any feeds routing content to it yet.</p>
<a href="{{ route('routing.create') }}" class="bg-blue-500 hover:bg-blue-700 text-white px-4 py-2 rounded text-sm">
Create Route
</a>
</div>
</div>
@endif
</div>
</div>
@endsection

View file

@ -1,249 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="flex justify-between items-center mb-6">
<h1 class="text-3xl font-bold text-gray-900">Dashboard</h1>
<!-- Time Period Selector -->
<form method="GET" class="flex items-center space-x-2">
<label for="period" class="text-sm font-medium text-gray-700">Period:</label>
<select name="period" id="period" onchange="this.form.submit()"
class="border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 text-sm">
@foreach($availablePeriods as $key => $label)
<option value="{{ $key }}" {{ $period === $key ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
</form>
</div>
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-6">
{{ session('success') }}
</div>
@endif
<!-- Article Stats -->
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<!-- Articles Fetched -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<x-heroicon-o-arrow-down-tray class="w-6 h-6 text-blue-600" />
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Articles Fetched</dt>
<dd class="text-lg font-medium text-gray-900">{{ number_format($stats['articles_fetched']) }}</dd>
</dl>
</div>
</div>
</div>
</div>
<!-- Articles Published -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<x-heroicon-o-paper-airplane class="w-6 h-6 text-green-600" />
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Articles Published</dt>
<dd class="text-lg font-medium text-gray-900">{{ number_format($stats['articles_published']) }}</dd>
</dl>
</div>
</div>
</div>
</div>
<!-- Published Percentage -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="p-5">
<div class="flex items-center">
<div class="flex-shrink-0">
<x-heroicon-o-chart-pie class="w-6 h-6 text-purple-600" />
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Published Rate</dt>
<dd class="text-lg font-medium text-gray-900">{{ $stats['published_percentage'] }}%</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- System Status Card -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
@if($systemStatus['is_enabled'])
<div class="w-8 h-8 bg-green-100 rounded-full flex items-center justify-center">
<x-heroicon-o-check-circle class="w-5 h-5 text-green-600" />
</div>
@else
<div class="w-8 h-8 bg-red-100 rounded-full flex items-center justify-center">
<x-heroicon-o-x-circle class="w-5 h-5 text-red-600" />
</div>
@endif
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">System Status</dt>
<dd class="text-lg font-medium {{ $systemStatus['status_class'] }}">
{{ $systemStatus['status'] }}
</dd>
</dl>
</div>
</div>
@if(!$systemStatus['is_enabled'] && count($systemStatus['reasons']) > 0)
<div class="mt-4 pt-4 border-t border-gray-200">
<h4 class="text-sm font-medium text-gray-700 mb-2">Reasons for being disabled:</h4>
<ul class="text-sm text-gray-600 space-y-1">
@foreach($systemStatus['reasons'] as $reason)
<li class="flex items-center">
<x-heroicon-o-exclamation-triangle class="w-4 h-4 text-yellow-500 mr-2 flex-shrink-0" />
{{ $reason }}
</li>
@endforeach
</ul>
</div>
@endif
@if(!$systemStatus['is_enabled'])
<div class="mt-4 pt-4 border-t border-gray-200">
<div class="flex space-x-3">
<a href="{{ route('settings.index') }}" class="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700">
<x-heroicon-o-cog-6-tooth class="w-4 h-4 mr-1" />
Settings
</a>
<a href="{{ route('feeds.index') }}" class="inline-flex items-center px-3 py-2 border border-gray-300 text-sm leading-4 font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50">
<x-heroicon-o-rss class="w-4 h-4 mr-1" />
Feeds
</a>
</div>
</div>
@endif
</div>
</div>
<!-- System Configuration -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center mb-4">
<div class="flex-shrink-0">
<x-heroicon-o-chart-bar class="w-6 h-6 text-gray-400" />
</div>
<div class="ml-5">
<h3 class="text-sm font-medium text-gray-500">System Configuration</h3>
</div>
</div>
<div class="space-y-3">
<div class="flex justify-between">
<span class="text-sm text-gray-600">Feeds</span>
<span class="text-sm font-medium text-gray-900">{{ $systemStats['active_feeds'] }}/{{ $systemStats['total_feeds'] }}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-gray-600">Channels</span>
<span class="text-sm font-medium text-gray-900">{{ $systemStats['active_channels'] }}/{{ $systemStats['total_channels'] }}</span>
</div>
<div class="flex justify-between">
<span class="text-sm text-gray-600">Routes</span>
<span class="text-sm font-medium text-gray-900">{{ $systemStats['active_routes'] }}/{{ $systemStats['total_routes'] }}</span>
</div>
</div>
</div>
</div>
<!-- Recent Activity -->
<div class="bg-white overflow-hidden shadow rounded-lg">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<x-heroicon-o-clock class="w-6 h-6 text-gray-400" />
</div>
<div class="ml-5 w-0 flex-1">
<dl>
<dt class="text-sm font-medium text-gray-500 truncate">Recent Activity</dt>
<dd class="text-lg font-medium text-gray-900">
<a href="{{ route('logs') }}" class="text-blue-600 hover:text-blue-800">View Logs</a>
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
<!-- Navigation Cards -->
<div class="mt-8 grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
<a href="{{ route('feeds.index') }}" class="bg-white overflow-hidden shadow rounded-lg hover:shadow-md transition-shadow">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<x-heroicon-o-rss class="w-8 h-8 text-orange-500" />
</div>
<div class="ml-5">
<h3 class="text-lg font-medium text-gray-900">Feeds</h3>
<p class="text-sm text-gray-500">Manage content sources</p>
</div>
</div>
</div>
</a>
<a href="{{ route('platforms.index') }}" class="bg-white overflow-hidden shadow rounded-lg hover:shadow-md transition-shadow">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<x-heroicon-o-share class="w-8 h-8 text-blue-500" />
</div>
<div class="ml-5">
<h3 class="text-lg font-medium text-gray-900">Platforms</h3>
<p class="text-sm text-gray-500">Manage platform accounts</p>
</div>
</div>
</div>
</a>
<a href="{{ route('channels.index') }}" class="bg-white overflow-hidden shadow rounded-lg hover:shadow-md transition-shadow">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<x-heroicon-o-hashtag class="w-8 h-8 text-purple-500" />
</div>
<div class="ml-5">
<h3 class="text-lg font-medium text-gray-900">Channels</h3>
<p class="text-sm text-gray-500">Manage publishing channels</p>
</div>
</div>
</div>
</a>
<a href="{{ route('routing.index') }}" class="bg-white overflow-hidden shadow rounded-lg hover:shadow-md transition-shadow">
<div class="px-4 py-5 sm:p-6">
<div class="flex items-center">
<div class="flex-shrink-0">
<x-heroicon-o-arrow-path class="w-8 h-8 text-green-500" />
</div>
<div class="ml-5">
<h3 class="text-lg font-medium text-gray-900">Routing</h3>
<p class="text-sm text-gray-500">Configure feed routing</p>
</div>
</div>
</div>
</a>
</div>
</div>
</div>
@endsection

View file

@ -1,109 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="max-w-2xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="mb-6">
<h1 class="text-2xl font-semibold text-gray-900">Add New Feed</h1>
<p class="mt-1 text-sm text-gray-600">Create a new content feed for articles.</p>
</div>
<div class="bg-white shadow sm:rounded-lg">
<form action="{{ route('feeds.store') }}" method="POST" class="px-4 py-5 sm:p-6">
@csrf
<div class="grid grid-cols-1 gap-6">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
<input type="text"
name="name"
id="name"
value="{{ old('name') }}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('name') border-red-300 @enderror"
placeholder="VRT News">
@error('name')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="url" class="block text-sm font-medium text-gray-700">URL</label>
<input type="url"
name="url"
id="url"
value="{{ old('url') }}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('url') border-red-300 @enderror"
placeholder="https://example.com or https://example.com/feed.xml">
@error('url')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="type" class="block text-sm font-medium text-gray-700">Type</label>
<select name="type"
id="type"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('type') border-red-300 @enderror">
<option value="">Select feed type...</option>
<option value="website" {{ old('type') === 'website' ? 'selected' : '' }}>Website</option>
<option value="rss" {{ old('type') === 'rss' ? 'selected' : '' }}>RSS Feed</option>
</select>
@error('type')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="language" class="block text-sm font-medium text-gray-700">Language</label>
<select name="language"
id="language"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('language') border-red-300 @enderror">
<option value="en" {{ old('language', 'en') === 'en' ? 'selected' : '' }}>English</option>
<option value="nl" {{ old('language') === 'nl' ? 'selected' : '' }}>Dutch</option>
<option value="fr" {{ old('language') === 'fr' ? 'selected' : '' }}>French</option>
<option value="de" {{ old('language') === 'de' ? 'selected' : '' }}>German</option>
<option value="es" {{ old('language') === 'es' ? 'selected' : '' }}>Spanish</option>
</select>
@error('language')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
<textarea name="description"
id="description"
rows="3"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('description') border-red-300 @enderror"
placeholder="Optional description of this feed...">{{ old('description') }}</textarea>
@error('description')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="flex items-center">
<input type="checkbox"
name="is_active"
id="is_active"
value="1"
{{ old('is_active', true) ? 'checked' : '' }}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="is_active" class="ml-2 block text-sm text-gray-900">
Active
</label>
</div>
</div>
<div class="mt-6 flex items-center justify-end space-x-3">
<a href="{{ route('feeds.index') }}" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cancel
</a>
<button type="submit" class="bg-indigo-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Create Feed
</button>
</div>
</form>
</div>
</div>
</div>
@endsection

View file

@ -1,110 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="max-w-2xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="mb-6">
<h1 class="text-2xl font-semibold text-gray-900">Edit Feed</h1>
<p class="mt-1 text-sm text-gray-600">Update the details for {{ $feed->name }}.</p>
</div>
<div class="bg-white shadow sm:rounded-lg">
<form action="{{ route('feeds.update', $feed) }}" method="POST" class="px-4 py-5 sm:p-6">
@csrf
@method('PUT')
<div class="grid grid-cols-1 gap-6">
<div>
<label for="name" class="block text-sm font-medium text-gray-700">Name</label>
<input type="text"
name="name"
id="name"
value="{{ old('name', $feed->name) }}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('name') border-red-300 @enderror"
placeholder="VRT News">
@error('name')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="url" class="block text-sm font-medium text-gray-700">URL</label>
<input type="url"
name="url"
id="url"
value="{{ old('url', $feed->url) }}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('url') border-red-300 @enderror"
placeholder="https://example.com or https://example.com/feed.xml">
@error('url')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="type" class="block text-sm font-medium text-gray-700">Type</label>
<select name="type"
id="type"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('type') border-red-300 @enderror">
<option value="">Select feed type...</option>
<option value="website" {{ old('type', $feed->type) === 'website' ? 'selected' : '' }}>Website</option>
<option value="rss" {{ old('type', $feed->type) === 'rss' ? 'selected' : '' }}>RSS Feed</option>
</select>
@error('type')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="language" class="block text-sm font-medium text-gray-700">Language</label>
<select name="language"
id="language"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('language') border-red-300 @enderror">
<option value="en" {{ old('language', $feed->language) === 'en' ? 'selected' : '' }}>English</option>
<option value="nl" {{ old('language', $feed->language) === 'nl' ? 'selected' : '' }}>Dutch</option>
<option value="fr" {{ old('language', $feed->language) === 'fr' ? 'selected' : '' }}>French</option>
<option value="de" {{ old('language', $feed->language) === 'de' ? 'selected' : '' }}>German</option>
<option value="es" {{ old('language', $feed->language) === 'es' ? 'selected' : '' }}>Spanish</option>
</select>
@error('language')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="description" class="block text-sm font-medium text-gray-700">Description</label>
<textarea name="description"
id="description"
rows="3"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('description') border-red-300 @enderror"
placeholder="Optional description of this feed...">{{ old('description', $feed->description) }}</textarea>
@error('description')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="flex items-center">
<input type="checkbox"
name="is_active"
id="is_active"
value="1"
{{ old('is_active', $feed->is_active) ? 'checked' : '' }}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="is_active" class="ml-2 block text-sm text-gray-900">
Active
</label>
</div>
</div>
<div class="mt-6 flex items-center justify-end space-x-3">
<a href="{{ route('feeds.index') }}" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cancel
</a>
<button type="submit" class="bg-indigo-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Update Feed
</button>
</div>
</form>
</div>
</div>
</div>
@endsection

View file

@ -1,99 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-semibold text-gray-900">Feeds</h1>
<a href="{{ route('feeds.create') }}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Add New Feed
</a>
</div>
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
{{ session('success') }}
</div>
@endif
<div class="bg-white shadow overflow-hidden sm:rounded-md">
@if($feeds->count() > 0)
<ul class="divide-y divide-gray-200">
@foreach($feeds as $feed)
<li>
<div class="px-4 py-4 flex items-center justify-between">
<div class="flex items-center">
<div class="flex-shrink-0">
@if($feed->type === 'rss')
<x-heroicon-o-rss class="w-5 h-5 text-orange-500" />
@else
<x-heroicon-o-globe-alt class="w-5 h-5 text-blue-500" />
@endif
</div>
<div class="ml-4">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900">{{ $feed->name }}</div>
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium
{{ $feed->is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
{{ $feed->is_active ? 'Active' : 'Inactive' }}
</span>
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
{{ $feed->type_display }}
</span>
<span class="ml-2 inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
{{ strtoupper($feed->language) }}
</span>
</div>
<div class="text-sm text-gray-500">{{ $feed->url }}</div>
@if($feed->description)
<div class="text-sm text-gray-500 mt-1">{{ Str::limit($feed->description, 100) }}</div>
@endif
<div class="text-xs text-gray-400 mt-1">{{ $feed->status }}</div>
</div>
</div>
<div class="flex items-center space-x-3">
<!-- Toggle Switch -->
<form action="{{ route('feeds.toggle', $feed) }}" method="POST" class="inline">
@csrf
<label class="inline-flex items-center cursor-pointer">
<span class="text-xs text-gray-600 mr-2">{{ $feed->is_active ? 'Active' : 'Inactive' }}</span>
<button type="submit" class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors {{ $feed->is_active ? 'bg-green-600' : 'bg-gray-200' }}">
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform {{ $feed->is_active ? 'translate-x-6' : 'translate-x-1' }}"></span>
</button>
</label>
</form>
<!-- Action Buttons -->
<a href="{{ route('feeds.show', $feed) }}" class="text-indigo-600 hover:text-indigo-900 text-sm font-medium">
View
</a>
<a href="{{ route('feeds.edit', $feed) }}" class="text-indigo-600 hover:text-indigo-900 text-sm font-medium">
Edit
</a>
<form action="{{ route('feeds.destroy', $feed) }}" method="POST" class="inline"
onsubmit="return confirm('Are you sure you want to delete this feed?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-900 text-sm font-medium">
Delete
</button>
</form>
</div>
</div>
</li>
@endforeach
</ul>
@else
<div class="text-center py-12">
<x-heroicon-o-rss class="w-24 h-24 text-gray-400 mb-4" />
<h3 class="text-lg font-medium text-gray-900 mb-2">No feeds yet</h3>
<p class="text-gray-500 mb-4">Get started by adding your first content feed.</p>
<a href="{{ route('feeds.create') }}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Add New Feed
</a>
</div>
@endif
</div>
</div>
</div>
@endsection

View file

@ -1,141 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="max-w-4xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="mb-6">
<div class="flex items-center justify-between">
<div>
<h1 class="text-2xl font-semibold text-gray-900">{{ $feed->name }}</h1>
<p class="mt-1 text-sm text-gray-600">{{ $feed->type_display }} {{ strtoupper($feed->language) }}</p>
</div>
<div class="flex items-center space-x-3">
<!-- Toggle Switch -->
<form action="{{ route('feeds.toggle', $feed) }}" method="POST" class="inline">
@csrf
<label class="inline-flex items-center cursor-pointer">
<span class="text-sm text-gray-700 mr-3">{{ $feed->is_active ? 'Active' : 'Inactive' }}</span>
<button type="submit" class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors {{ $feed->is_active ? 'bg-green-600' : 'bg-gray-200' }}">
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform {{ $feed->is_active ? 'translate-x-6' : 'translate-x-1' }}"></span>
</button>
</label>
</form>
<a href="{{ route('feeds.edit', $feed) }}" class="bg-indigo-600 hover:bg-indigo-700 text-white font-bold py-2 px-4 rounded">
Edit Feed
</a>
</div>
</div>
</div>
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-6">
{{ session('success') }}
</div>
@endif
@if(!$feed->is_active)
<div class="mb-6 bg-yellow-50 border border-yellow-200 rounded-md p-4">
<div class="flex">
<div class="flex-shrink-0">
<x-heroicon-o-exclamation-triangle class="w-5 h-5 text-yellow-400" />
</div>
<div class="ml-3">
<p class="text-sm text-yellow-800">
This feed is inactive. No articles will be fetched or published from this feed until reactivated.
</p>
</div>
</div>
</div>
@endif
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 sm:px-6">
<h3 class="text-lg leading-6 font-medium text-gray-900">Feed Details</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Information about this content feed.</p>
</div>
<div class="border-t border-gray-200 px-4 py-5 sm:px-6">
<dl class="grid grid-cols-1 gap-x-4 gap-y-6 sm:grid-cols-2">
<div>
<dt class="text-sm font-medium text-gray-500">Name</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $feed->name }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Type</dt>
<dd class="mt-1 text-sm text-gray-900">
<div class="flex items-center">
@if($feed->type === 'rss')
<x-heroicon-o-rss class="w-4 h-4 text-orange-500 mr-2" />
@else
<x-heroicon-o-globe-alt class="w-4 h-4 text-blue-500 mr-2" />
@endif
{{ $feed->type_display }}
</div>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">URL</dt>
<dd class="mt-1 text-sm text-gray-900">
<a href="{{ $feed->url }}" target="_blank" class="text-indigo-600 hover:text-indigo-500">
{{ $feed->url }}
<x-heroicon-o-arrow-top-right-on-square class="w-3 h-3 ml-1" />
</a>
</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Language</dt>
<dd class="mt-1 text-sm text-gray-900">{{ strtoupper($feed->language) }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Status</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $feed->status }}</dd>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">Created</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $feed->created_at->format('M j, Y g:i A') }}</dd>
</div>
@if($feed->last_fetched_at)
<div>
<dt class="text-sm font-medium text-gray-500">Last Fetched</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $feed->last_fetched_at->format('M j, Y g:i A') }}</dd>
</div>
@endif
@if($feed->description)
<div class="sm:col-span-2">
<dt class="text-sm font-medium text-gray-500">Description</dt>
<dd class="mt-1 text-sm text-gray-900">{{ $feed->description }}</dd>
</div>
@endif
</dl>
</div>
</div>
<div class="mt-6 flex items-center justify-between">
<a href="{{ route('feeds.index') }}" class="text-indigo-600 hover:text-indigo-500 font-medium">
Back to Feeds
</a>
<div class="flex items-center space-x-3">
<a href="{{ route('feeds.edit', $feed) }}" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50">
Edit
</a>
<form action="{{ route('feeds.destroy', $feed) }}" method="POST" class="inline"
onsubmit="return confirm('Are you sure you want to delete this feed?')">
@csrf
@method('DELETE')
<button type="submit" class="bg-red-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-red-700">
Delete Feed
</button>
</form>
</div>
</div>
</div>
</div>
@endsection

View file

@ -1,40 +0,0 @@
@extends('layouts.app')
@section('page-title', 'System Logs')
@section('content')
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Recent Logs</h2>
</div>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Level</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Message</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Created At</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach($logs as $log)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ $log->id }}</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full
{{ $log->level->value === 'error' ? 'bg-red-100 text-red-800' :
($log->level->value === 'warning' ? 'bg-yellow-100 text-yellow-800' : 'bg-green-100 text-green-800') }}">
{{ ucfirst($log->level->value) }}
</span>
</td>
<td class="px-6 py-4 text-sm text-gray-900">{{ Str::limit($log->message, 100) }}</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">{{ $log->created_at->format('Y-m-d H:i') }}</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endsection

View file

@ -1,103 +0,0 @@
@extends('layouts.app')
@section('page-title', 'Platform Accounts')
@section('content')
<div class="mb-6">
<a href="{{ route('platforms.create') }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg inline-flex items-center">
<x-heroicon-o-plus class="w-4 h-4 mr-2" />
Add Platform Account
</a>
</div>
<div class="bg-white rounded-lg shadow">
<div class="px-6 py-4 border-b border-gray-200">
<h2 class="text-lg font-medium text-gray-900">Platform Accounts</h2>
<p class="text-sm text-gray-600 mt-1">Manage your social media platform accounts for posting</p>
</div>
@if($accounts->isEmpty())
<div class="p-6 text-center">
<x-heroicon-o-share class="w-16 h-16 text-gray-400 mb-4" />
<h3 class="text-lg font-medium text-gray-900 mb-2">No platform accounts configured</h3>
<p class="text-gray-600 mb-4">Add your first platform account to start posting articles</p>
<a href="{{ route('platforms.create') }}" class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded-lg inline-flex items-center">
<x-heroicon-o-plus class="w-4 h-4 mr-2" />
Add Platform Account
</a>
</div>
@else
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200">
<thead class="bg-gray-50">
<tr>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Platform</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Account</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Last Tested</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody class="bg-white divide-y divide-gray-200">
@foreach($accounts as $account)
<tr class="hover:bg-gray-50">
<td class="px-6 py-4 whitespace-nowrap">
<div class="flex items-center">
<x-heroicon-o-globe-alt class="w-5 h-5 mr-3
{{ $account->platform === 'lemmy' ? 'text-orange-500' :
($account->platform === 'mastodon' ? 'text-purple-500' : 'text-red-500') }}" />
<span class="text-sm font-medium text-gray-900 capitalize">{{ $account->platform }}</span>
@if($account->is_active)
<span class="ml-2 inline-flex px-2 py-1 text-xs font-semibold rounded-full bg-green-100 text-green-800">
Active
</span>
@endif
</div>
</td>
<td class="px-6 py-4">
<div class="text-sm text-gray-900">{{ $account->username }}</div>
<div class="text-sm text-gray-500">{{ $account->instance_url }}</div>
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="inline-flex px-2 py-1 text-xs font-semibold rounded-full
{{ $account->status === 'working' ? 'bg-green-100 text-green-800' :
($account->status === 'failed' ? 'bg-red-100 text-red-800' : 'bg-gray-100 text-gray-800') }}">
{{ ucfirst($account->status) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ $account->last_tested_at ? $account->last_tested_at->format('Y-m-d H:i') : 'Never' }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<div class="flex space-x-2">
@if(!$account->is_active)
<form action="{{ route('platforms.set-active', $account) }}" method="POST" class="inline">
@csrf
<button type="submit" class="text-green-600 hover:text-green-900">
<x-heroicon-o-check class="w-4 h-4 mr-1" />Set Active
</button>
</form>
@endif
<a href="{{ route('platforms.edit', $account) }}" class="text-blue-600 hover:text-blue-900">
<x-heroicon-o-pencil class="w-4 h-4 mr-1" />Edit
</a>
<form action="{{ route('platforms.destroy', $account) }}" method="POST" class="inline"
onsubmit="return confirm('Are you sure you want to delete this platform account?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-900">
<x-heroicon-o-trash class="w-4 h-4 mr-1" />Delete
</button>
</form>
</div>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
@endif
</div>
@endsection

View file

@ -1,159 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="max-w-2xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="mb-6">
<h1 class="text-2xl font-semibold text-gray-900">Create Feed Routing</h1>
<p class="mt-1 text-sm text-gray-600">Route a feed to one or more channels.</p>
</div>
<div class="bg-white shadow sm:rounded-lg">
<form action="{{ route('routing.store') }}" method="POST" class="px-4 py-5 sm:p-6">
@csrf
<div class="grid grid-cols-1 gap-6">
<div>
<label for="feed_id" class="block text-sm font-medium text-gray-700">Feed</label>
<select name="feed_id"
id="feed_id"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('feed_id') border-red-300 @enderror">
<option value="">Select a feed...</option>
@foreach($feeds as $feed)
<option value="{{ $feed->id }}" {{ old('feed_id') == $feed->id ? 'selected' : '' }}>
{{ $feed->name }} ({{ $feed->type_display }})
</option>
@endforeach
</select>
@error('feed_id')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">Target Channels</label>
<div class="space-y-2 max-h-64 overflow-y-auto border border-gray-200 rounded-md p-3">
@forelse($channels as $channel)
<div class="flex items-center">
<input type="checkbox"
name="channel_ids[]"
value="{{ $channel->id }}"
id="channel_{{ $channel->id }}"
{{ in_array($channel->id, old('channel_ids', [])) ? 'checked' : '' }}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="channel_{{ $channel->id }}" class="ml-2 block text-sm text-gray-900">
<span class="font-medium">{{ $channel->name }}</span>
<span class="text-gray-500">({{ $channel->platformInstance->name }})</span>
</label>
</div>
@empty
<p class="text-sm text-gray-500">No active channels available. Please create channels first.</p>
@endforelse
</div>
@error('channel_ids')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
@error('channel_ids.*')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="priority" class="block text-sm font-medium text-gray-700">Priority</label>
<input type="number"
name="priority"
id="priority"
min="0"
max="100"
value="{{ old('priority', 0) }}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('priority') border-red-300 @enderror"
placeholder="0">
<p class="mt-1 text-sm text-gray-500">Higher numbers = higher priority (0-100)</p>
@error('priority')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-3">Keywords (Optional)</label>
<div id="keywords-container" class="space-y-2">
<div class="keyword-input-group flex items-center space-x-2">
<input type="text"
name="keywords[]"
class="flex-1 border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm"
placeholder="Enter keyword">
<button type="button"
class="remove-keyword bg-red-500 hover:bg-red-600 text-white px-3 py-2 rounded-md text-sm"
onclick="removeKeyword(this)">
Remove
</button>
</div>
</div>
<button type="button"
id="add-keyword"
class="mt-2 bg-green-500 hover:bg-green-600 text-white px-3 py-2 rounded-md text-sm"
onclick="addKeyword()">
Add Keyword
</button>
<p class="mt-1 text-sm text-gray-500">Articles will be filtered to only include content matching these keywords</p>
@error('keywords')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
@error('keywords.*')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="filters" class="block text-sm font-medium text-gray-700">Other Filters (Optional)</label>
<textarea name="filters"
id="filters"
rows="4"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('filters') border-red-300 @enderror"
placeholder='{"exclude_keywords": ["sports"], "min_length": 100}'>{{ old('filters') }}</textarea>
<p class="mt-1 text-sm text-gray-500">JSON format for additional content filtering rules</p>
@error('filters')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
</div>
<div class="mt-6 flex items-center justify-end space-x-3">
<a href="{{ route('routing.index') }}" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cancel
</a>
<button type="submit" class="bg-indigo-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Create Routing
</button>
</div>
</form>
</div>
@if($feeds->isEmpty() || $channels->isEmpty())
<div class="mt-6 bg-yellow-50 border border-yellow-200 rounded-md p-4">
<div class="flex">
<div class="flex-shrink-0">
<x-heroicon-o-exclamation-triangle class="w-5 h-5 text-yellow-400" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">Prerequisites Missing</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>To create routing, you need:</p>
<ul class="list-disc list-inside mt-1">
@if($feeds->isEmpty())
<li>At least one active <a href="{{ route('feeds.create') }}" class="underline">feed</a></li>
@endif
@if($channels->isEmpty())
<li>At least one active <a href="{{ route('channels.create') }}" class="underline">channel</a></li>
@endif
</ul>
</div>
</div>
</div>
</div>
@endif
</div>
</div>
@vite('resources/js/routing.js')
@endsection

View file

@ -1,131 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="max-w-2xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="mb-6">
<h1 class="text-2xl font-semibold text-gray-900">Edit Routing</h1>
<p class="mt-1 text-sm text-gray-600">
{{ $feed->name }} {{ $channel->name }} ({{ $channel->platformInstance->name }})
</p>
</div>
<div class="bg-white shadow sm:rounded-lg">
<form action="{{ route('routing.update', [$feed, $channel]) }}" method="POST" class="px-4 py-5 sm:p-6">
@csrf
@method('PUT')
<div class="grid grid-cols-1 gap-6">
<div>
<h3 class="text-lg font-medium text-gray-900 mb-4">Routing Details</h3>
<div class="bg-gray-50 rounded-lg p-4">
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<span class="font-medium text-gray-700">Feed:</span>
<div class="flex items-center mt-1">
@if($feed->type === 'rss')
<x-heroicon-o-rss class="w-4 h-4 text-orange-500 mr-2" />
@else
<x-heroicon-o-globe-alt class="w-4 h-4 text-blue-500 mr-2" />
@endif
{{ $feed->name }}
</div>
</div>
<div>
<span class="font-medium text-gray-700">Channel:</span>
<div class="flex items-center mt-1">
<x-heroicon-o-hashtag class="w-4 h-4 text-gray-400 mr-2" />
{{ $channel->name }}
<span class="ml-2 text-gray-500">({{ $channel->platformInstance->name }})</span>
</div>
</div>
</div>
</div>
</div>
<div class="flex items-center">
<input type="checkbox"
name="is_active"
id="is_active"
value="1"
{{ old('is_active', $routing->pivot->is_active) ? 'checked' : '' }}
class="h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 rounded">
<label for="is_active" class="ml-2 block text-sm text-gray-900">
Active
</label>
</div>
<div>
<label for="priority" class="block text-sm font-medium text-gray-700">Priority</label>
<input type="number"
name="priority"
id="priority"
min="0"
max="100"
value="{{ old('priority', $routing->pivot->priority) }}"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('priority') border-red-300 @enderror"
placeholder="0">
<p class="mt-1 text-sm text-gray-500">Higher numbers = higher priority (0-100)</p>
@error('priority')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div>
<label for="filters" class="block text-sm font-medium text-gray-700">Filters</label>
<textarea name="filters"
id="filters"
rows="6"
class="mt-1 block w-full border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm @error('filters') border-red-300 @enderror"
placeholder='{"keywords": ["technology", "AI"], "exclude_keywords": ["sports"]}'>{{ old('filters', $routing->pivot->filters ? json_encode($routing->pivot->filters, JSON_PRETTY_PRINT) : '') }}</textarea>
<p class="mt-1 text-sm text-gray-500">JSON format for content filtering rules</p>
@error('filters')
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
@enderror
</div>
<div class="bg-blue-50 border border-blue-200 rounded-md p-4">
<div class="flex">
<div class="flex-shrink-0">
<x-heroicon-o-information-circle class="w-5 h-5 text-blue-400" />
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-blue-800">Filter Examples</h3>
<div class="mt-2 text-sm text-blue-700">
<p>You can use filters to control which content gets routed:</p>
<ul class="list-disc list-inside mt-1 space-y-1">
<li><code>{"keywords": ["tech", "AI"]}</code> - Include only articles with these keywords</li>
<li><code>{"exclude_keywords": ["sports"]}</code> - Exclude articles with these keywords</li>
<li><code>{"min_length": 500}</code> - Minimum article length</li>
<li><code>{"max_age_hours": 24}</code> - Only articles from last 24 hours</li>
</ul>
</div>
</div>
</div>
</div>
</div>
<div class="mt-6 flex items-center justify-between">
<form action="{{ route('routing.destroy', [$feed, $channel]) }}" method="POST" class="inline"
onsubmit="return confirm('Are you sure you want to delete this routing?')">
@csrf
@method('DELETE')
<button type="submit" class="bg-red-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-red-700">
Delete Routing
</button>
</form>
<div class="flex items-center space-x-3">
<a href="{{ route('routing.index') }}" class="bg-white py-2 px-4 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Cancel
</a>
<button type="submit" class="bg-indigo-600 py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Update Routing
</button>
</div>
</div>
</form>
</div>
</div>
</div>
@endsection

View file

@ -1,154 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="flex justify-between items-center mb-6">
<div>
<h1 class="text-2xl font-semibold text-gray-900">Feed Routing</h1>
<p class="mt-1 text-sm text-gray-600">Manage how feeds are routed to channels</p>
</div>
<a href="{{ route('routing.create') }}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Create New Routing
</a>
</div>
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
{{ session('success') }}
</div>
@endif
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Feeds Section -->
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
<h3 class="text-lg leading-6 font-medium text-gray-900">Feeds Channels</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Active feed-to-channel routing</p>
</div>
<div class="divide-y divide-gray-200">
@forelse($feeds as $feed)
@if($feed->channels->count() > 0)
<div class="px-4 py-4">
<div class="flex items-center mb-3">
@if($feed->type === 'rss')
<x-heroicon-o-rss class="w-4 h-4 text-orange-500 mr-2" />
@else
<x-heroicon-o-globe-alt class="w-4 h-4 text-blue-500 mr-2" />
@endif
<span class="font-medium text-gray-900">{{ $feed->name }}</span>
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
{{ $feed->channels->count() }} channel{{ $feed->channels->count() !== 1 ? 's' : '' }}
</span>
</div>
<div class="space-y-2">
@foreach($feed->channels as $channel)
<div class="flex items-center justify-between bg-gray-50 rounded px-3 py-2">
<div class="flex items-center">
<x-heroicon-o-hashtag class="w-4 h-4 text-gray-400 mr-2" />
<span class="text-sm text-gray-900">{{ $channel->name }}</span>
<span class="ml-2 text-xs text-gray-500">({{ $channel->platformInstance->name }})</span>
@if(!$channel->pivot->is_active)
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
Inactive
</span>
@endif
@if($channel->pivot->priority > 0)
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
Priority: {{ $channel->pivot->priority }}
</span>
@endif
</div>
<div class="flex items-center space-x-2">
<form action="{{ route('routing.toggle', [$feed, $channel]) }}" method="POST" class="inline">
@csrf
<label class="inline-flex items-center cursor-pointer">
<span class="text-xs text-gray-600 mr-2">{{ $channel->pivot->is_active ? 'Active' : 'Inactive' }}</span>
<button type="submit" class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors {{ $channel->pivot->is_active ? 'bg-green-600' : 'bg-gray-200' }}">
<span class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform {{ $channel->pivot->is_active ? 'translate-x-6' : 'translate-x-1' }}"></span>
</button>
</label>
</form>
<a href="{{ route('routing.edit', [$feed, $channel]) }}" class="text-indigo-600 hover:text-indigo-900 text-xs">Edit</a>
<form action="{{ route('routing.destroy', [$feed, $channel]) }}" method="POST" class="inline"
onsubmit="return confirm('Remove this routing?')">
@csrf
@method('DELETE')
<button type="submit" class="text-red-600 hover:text-red-900 text-xs">Remove</button>
</form>
</div>
</div>
@endforeach
</div>
</div>
@endif
@empty
<div class="px-4 py-8 text-center text-gray-500">
No feed routing configured
</div>
@endforelse
</div>
</div>
<!-- Channels Section -->
<div class="bg-white shadow overflow-hidden sm:rounded-lg">
<div class="px-4 py-5 sm:px-6 border-b border-gray-200">
<h3 class="text-lg leading-6 font-medium text-gray-900">Channels Feeds</h3>
<p class="mt-1 max-w-2xl text-sm text-gray-500">Channels and their connected feeds</p>
</div>
<div class="divide-y divide-gray-200">
@forelse($channels as $channel)
@if($channel->feeds->count() > 0)
<div class="px-4 py-4">
<div class="flex items-center mb-3">
<x-heroicon-o-hashtag class="w-4 h-4 text-gray-400 mr-2" />
<span class="font-medium text-gray-900">{{ $channel->name }}</span>
<span class="ml-2 text-xs text-gray-500">({{ $channel->platformInstance->name }})</span>
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800">
{{ $channel->feeds->count() }} feed{{ $channel->feeds->count() !== 1 ? 's' : '' }}
</span>
</div>
<div class="space-y-1">
@foreach($channel->feeds as $feed)
<div class="flex items-center justify-between bg-gray-50 rounded px-3 py-2">
<div class="flex items-center">
@if($feed->type === 'rss')
<x-heroicon-o-rss class="w-4 h-4 text-orange-500 mr-2" />
@else
<x-heroicon-o-globe-alt class="w-4 h-4 text-blue-500 mr-2" />
@endif
<span class="text-sm text-gray-900">{{ $feed->name }}</span>
@if(!$feed->pivot->is_active)
<span class="ml-2 inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
Inactive
</span>
@endif
</div>
<a href="{{ route('routing.edit', [$feed, $channel]) }}" class="text-indigo-600 hover:text-indigo-900 text-xs">Edit</a>
</div>
@endforeach
</div>
</div>
@endif
@empty
<div class="px-4 py-8 text-center text-gray-500">
No channels with feeds
</div>
@endforelse
</div>
</div>
</div>
@if($feeds->where('channels')->isEmpty() && $channels->where('feeds')->isEmpty())
<div class="text-center py-12">
<x-heroicon-o-arrow-path class="w-24 h-24 text-gray-400 mb-4" />
<h3 class="text-lg font-medium text-gray-900 mb-2">No routing configured</h3>
<p class="text-gray-500 mb-4">Connect your feeds to channels to start routing content.</p>
<a href="{{ route('routing.create') }}" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Create First Routing
</a>
</div>
@endif
</div>
</div>
@endsection

View file

@ -1,80 +0,0 @@
@extends('layouts.app')
@section('content')
<div class="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8">
<div class="px-4 py-6 sm:px-0">
<div class="flex justify-between items-center mb-6">
<h1 class="text-2xl font-semibold text-gray-900">Settings</h1>
</div>
@if(session('success'))
<div class="bg-green-100 border border-green-400 text-green-700 px-4 py-3 rounded mb-4">
{{ session('success') }}
</div>
@endif
<div class="bg-white shadow overflow-hidden sm:rounded-md">
<div class="px-4 py-5 sm:p-6">
<form action="{{ route('settings.update') }}" method="POST">
@csrf
@method('PUT')
<div class="mb-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Article Processing</h3>
<div class="flex items-center justify-between">
<div class="flex-1">
<label class="text-sm font-medium text-gray-700">Enable Article Processing</label>
<p class="text-sm text-gray-500 mt-1">When disabled, the system will not fetch new articles or publish them to platforms.</p>
</div>
<div class="ml-4">
<label class="inline-flex items-center">
<input type="hidden" name="article_processing_enabled" value="0">
<input type="checkbox"
name="article_processing_enabled"
value="1"
{{ $articleProcessingEnabled ? 'checked' : '' }}
class="form-checkbox h-5 w-5 text-blue-600">
<span class="ml-2 text-sm text-gray-700">
{{ $articleProcessingEnabled ? 'Enabled' : 'Disabled' }}
</span>
</label>
</div>
</div>
</div>
<div class="mb-6">
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">Publishing Control</h3>
<div class="flex items-center justify-between">
<div class="flex-1">
<label class="text-sm font-medium text-gray-700">Enable Publishing Approvals</label>
<p class="text-sm text-gray-500 mt-1">When enabled, articles will require manual approval before being published to platforms.</p>
</div>
<div class="ml-4">
<label class="inline-flex items-center">
<input type="hidden" name="enable_publishing_approvals" value="0">
<input type="checkbox"
name="enable_publishing_approvals"
value="1"
{{ $publishingApprovalsEnabled ? 'checked' : '' }}
class="form-checkbox h-5 w-5 text-blue-600">
<span class="ml-2 text-sm text-gray-700">
{{ $publishingApprovalsEnabled ? 'Enabled' : 'Disabled' }}
</span>
</label>
</div>
</div>
</div>
<div class="flex justify-end">
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Save Settings
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endsection

View file

@ -1,10 +0,0 @@
<nav>
<ul>
<li>
<a href="/articles">Articles</a>
</li>
<li>
<a href="/logs">Logs</a>
</li>
</ul>
</nav>

View file

@ -1,41 +0,0 @@
<div class="bg-gray-800 text-white w-64 flex-shrink-0">
<div class="p-4">
<h2 class="text-xl font-bold">Lemmy Poster</h2>
<p class="text-gray-400 text-sm">Admin Panel</p>
</div>
<nav class="mt-8">
<a href="/articles" class="flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors {{ request()->is('articles') ? 'bg-gray-700 text-white' : '' }}">
<x-heroicon-o-newspaper class="w-5 h-5 mr-3" />
Articles
</a>
<a href="/platforms" class="flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors {{ request()->is('platforms*') ? 'bg-gray-700 text-white' : '' }}">
<x-heroicon-o-share class="w-5 h-5 mr-3" />
Platforms
</a>
<a href="/channels" class="flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors {{ request()->is('channels*') ? 'bg-gray-700 text-white' : '' }}">
<x-heroicon-o-hashtag class="w-5 h-5 mr-3" />
Channels
</a>
<a href="/feeds" class="flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors {{ request()->is('feeds*') ? 'bg-gray-700 text-white' : '' }}">
<x-heroicon-o-rss class="w-5 h-5 mr-3" />
Feeds
</a>
<a href="/routing" class="flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors {{ request()->is('routing*') ? 'bg-gray-700 text-white' : '' }}">
<x-heroicon-o-arrow-path class="w-5 h-5 mr-3" />
Routing
</a>
<a href="/logs" class="flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors {{ request()->is('logs') ? 'bg-gray-700 text-white' : '' }}">
<x-heroicon-o-list-bullet class="w-5 h-5 mr-3" />
Logs
</a>
<a href="/settings" class="flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors {{ request()->is('settings*') ? 'bg-gray-700 text-white' : '' }}">
<x-heroicon-o-cog-6-tooth class="w-5 h-5 mr-3" />
Settings
</a>
<a href="/horizon" class="flex items-center px-4 py-3 text-gray-300 hover:bg-gray-700 hover:text-white transition-colors">
<x-heroicon-o-chart-bar class="w-5 h-5 mr-3" />
Queue Monitor
</a>
</nav>
</div>

View file

@ -1,157 +0,0 @@
#!/bin/bash
# FFR Regression Test Suite Runner
# Comprehensive test runner for the FFR application
set -e
echo "🧪 Starting FFR Regression Test Suite"
echo "======================================"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Function to print colored output
print_status() {
echo -e "${BLUE} $1${NC}"
}
print_success() {
echo -e "${GREEN}$1${NC}"
}
print_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
print_error() {
echo -e "${RED}$1${NC}"
}
# Check if we're in the right directory
if [ ! -f "composer.json" ] || [ ! -f "phpunit.xml" ]; then
print_error "This script must be run from the project root directory"
exit 1
fi
# Set test environment
export APP_ENV=testing
print_status "Setting up test environment..."
# Clear configuration cache
php artisan config:clear --env=testing
# Ensure database is set up for testing
print_status "Preparing test database..."
php artisan migrate:fresh --env=testing --force
# Run database seeders for testing if they exist
if [ -f "database/seeders/TestSeeder.php" ]; then
php artisan db:seed --class=TestSeeder --env=testing
fi
echo ""
print_status "Running regression tests..."
echo ""
# Track test results
TOTAL_TESTS=0
FAILED_TESTS=0
# Function to run test suite and track results
run_test_suite() {
local suite_name="$1"
local test_path="$2"
local description="$3"
echo ""
print_status "Running $suite_name..."
echo "Description: $description"
if php artisan test "$test_path" --env=testing; then
print_success "$suite_name passed"
else
print_error "$suite_name failed"
FAILED_TESTS=$((FAILED_TESTS + 1))
fi
TOTAL_TESTS=$((TOTAL_TESTS + 1))
}
# Run individual test suites
run_test_suite "API Endpoint Tests" "tests/Feature/ApiEndpointRegressionTest.php" "Tests all HTTP endpoints and routes"
run_test_suite "Database Integration Tests" "tests/Feature/DatabaseIntegrationTest.php" "Tests models, relationships, and database operations"
run_test_suite "Article Discovery Tests" "tests/Feature/ArticleDiscoveryCommandTest.php" "Tests article discovery command functionality"
run_test_suite "Article Publishing Tests" "tests/Feature/ArticlePublishingTest.php" "Tests article publishing workflow"
run_test_suite "Jobs and Events Tests" "tests/Feature/JobsAndEventsTest.php" "Tests queue jobs and event handling"
run_test_suite "Authentication & Authorization Tests" "tests/Feature/AuthenticationAndAuthorizationTest.php" "Tests security and access control"
run_test_suite "New Article Fetched Event Tests" "tests/Feature/NewArticleFetchedEventTest.php" "Tests new article event handling"
run_test_suite "Validate Article Listener Tests" "tests/Feature/ValidateArticleListenerTest.php" "Tests article validation logic"
# Run Unit Tests
echo ""
print_status "Running Unit Tests..."
run_test_suite "Article Fetcher Unit Tests" "tests/Unit/Services/ArticleFetcherTest.php" "Tests article fetching service"
run_test_suite "Validation Service Unit Tests" "tests/Unit/Services/ValidationServiceTest.php" "Tests article validation service"
run_test_suite "Dashboard Stats Service Unit Tests" "tests/Unit/Services/DashboardStatsServiceTest.php" "Tests dashboard statistics service"
# Run full test suite for coverage
echo ""
print_status "Running complete test suite for coverage..."
if php artisan test --coverage-text --min=70 --env=testing; then
print_success "Test coverage meets minimum requirements"
else
print_warning "Test coverage below 70% - consider adding more tests"
fi
# Performance Tests
echo ""
print_status "Running performance checks..."
# Test database query performance
php artisan test tests/Feature/DatabaseIntegrationTest.php --env=testing --stop-on-failure
# Memory usage test
if php -d memory_limit=128M artisan test --env=testing --stop-on-failure; then
print_success "Memory usage within acceptable limits"
else
print_warning "High memory usage detected during tests"
fi
# Final Results
echo ""
echo "======================================"
print_status "Test Suite Complete"
echo "======================================"
if [ $FAILED_TESTS -eq 0 ]; then
print_success "All $TOTAL_TESTS test suites passed! 🎉"
echo ""
echo "Regression test suite completed successfully."
echo "The application is ready for deployment."
exit 0
else
print_error "$FAILED_TESTS out of $TOTAL_TESTS test suites failed"
echo ""
echo "Please review the failing tests before proceeding."
echo "Run individual test suites with:"
echo " php artisan test tests/Feature/[TestFile].php"
echo " php artisan test tests/Unit/[TestFile].php"
exit 1
fi

View file

@ -1,122 +0,0 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */
"module": "ESNext" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "bundler" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
// "rewriteRelativeImportExtensions": true, /* Rewrite '.ts', '.tsx', '.mts', and '.cts' file extensions in relative import paths to their JavaScript equivalent in output files. */
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
// "resolveJsonModule": true, /* Enable importing .json files. */
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */,
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
"noEmit": true /* Disable emitting files from a compilation. */,
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
// "outDir": "./", /* Specify an output folder for all emitted files. */
// "removeComments": true, /* Disable emitting comments. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
// "newLine": "crlf", /* Set the newline character for emitting files. */
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
"isolatedModules": true /* Ensure that each file can be safely transpiled without relying on other imports. */,
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
"skipLibCheck": true /* Skip type checking all .d.ts files. */,
"baseUrl": ".",
"paths": {
"@/*": ["./resources/js/*"],
"ziggy-js": ["./vendor/tightenco/ziggy"]
},
"jsx": "react-jsx"
},
"include": [
"resources/js/**/*.ts",
"resources/js/**/*.d.ts",
"resources/js/**/*.tsx",
]
}

View file

@ -1,41 +0,0 @@
import { defineConfig } from 'vite';
import laravel from 'laravel-vite-plugin';
import react from '@vitejs/plugin-react';
export default defineConfig({
plugins: [
react(),
laravel({
input: [
'resources/css/app.css',
'resources/js/app.tsx',
],
refresh: true,
}),
],
server: {
host: '0.0.0.0',
port: 5173,
hmr: {
host: 'localhost',
},
watch: {
usePolling: true,
interval: 1000,
},
},
esbuild: {
target: 'es2020',
jsx: 'automatic',
},
resolve: {
extensions: ['.tsx', '.ts', '.jsx', '.js'],
},
optimizeDeps: {
include: ['react', 'react-dom'],
esbuildOptions: {
target: 'es2020',
jsx: 'automatic',
},
},
});