Clean up root dir
This commit is contained in:
parent
da857b7951
commit
a7108ce17c
48 changed files with 0 additions and 4664 deletions
66
Dockerfile
66
Dockerfile
|
|
@ -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"]
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
];
|
||||
26
package.json
26
package.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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();
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<nav>
|
||||
<ul>
|
||||
<li>
|
||||
<a href="/articles">Articles</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/logs">Logs</a>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
|
||||
122
tsconfig.json
122
tsconfig.json
|
|
@ -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",
|
||||
]
|
||||
}
|
||||
|
|
@ -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',
|
||||
},
|
||||
},
|
||||
});
|
||||
Loading…
Reference in a new issue