- Migrate from React/TypeScript to vanilla HTML/CSS/JS with Alpine.js - Implement all original functionality: authentication, media queue, OAuth flow - Add auto-loading of media queue on dashboard access - Enhance JWT expiration handling with proper redirects - Improve OAuth callback page with beautiful UI and status updates - Remove unused HTMX dependency - Clean up old React project files - Update README with live demo link and development instructions Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
		
			
				
	
	
		
			301 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			301 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
<!DOCTYPE html>
 | 
						|
<html lang="en">
 | 
						|
<head>
 | 
						|
    <meta charset="UTF-8" />
 | 
						|
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
 | 
						|
    <title>Admin Dashboard v2</title>
 | 
						|
    <meta name="description" content="Admin Dashboard for Media Review" />
 | 
						|
    <meta name="author" content="" />
 | 
						|
 | 
						|
    <meta property="og:title" content="Admin Dashboard v2" />
 | 
						|
    <meta property="og:description" content="Admin Dashboard for Media Review" />
 | 
						|
    <meta property="og:type" content="website" />
 | 
						|
 | 
						|
    <meta name="twitter:card" content="summary_large_image" />
 | 
						|
    
 | 
						|
    <!-- Tailwind CSS -->
 | 
						|
    <script src="https://cdn.tailwindcss.com"></script>
 | 
						|
    
 | 
						|
    <!-- Alpine.js -->
 | 
						|
    <script defer src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.min.js"></script>
 | 
						|
    
 | 
						|
 | 
						|
    
 | 
						|
    <link rel="stylesheet" href="./css/style.css">
 | 
						|
    
 | 
						|
    <script>
 | 
						|
        // Configure tailwind
 | 
						|
        tailwind.config = {
 | 
						|
            darkMode: 'class',
 | 
						|
            theme: {
 | 
						|
                extend: {
 | 
						|
                    colors: {
 | 
						|
                        border: "hsl(var(--border))",
 | 
						|
                        input: "hsl(var(--input))",
 | 
						|
                        ring: "hsl(var(--ring))",
 | 
						|
                        background: "hsl(var(--background))",
 | 
						|
                        foreground: "hsl(var(--foreground))",
 | 
						|
                        primary: {
 | 
						|
                            DEFAULT: "hsl(var(--primary))",
 | 
						|
                            foreground: "hsl(var(--primary-foreground))",
 | 
						|
                        },
 | 
						|
                        secondary: {
 | 
						|
                            DEFAULT: "hsl(var(--secondary))",
 | 
						|
                            foreground: "hsl(var(--secondary-foreground))",
 | 
						|
                        },
 | 
						|
                        destructive: {
 | 
						|
                            DEFAULT: "hsl(var(--destructive))",
 | 
						|
                            foreground: "hsl(var(--destructive-foreground))",
 | 
						|
                        },
 | 
						|
                        muted: {
 | 
						|
                            DEFAULT: "hsl(var(--muted))",
 | 
						|
                            foreground: "hsl(var(--muted-foreground))",
 | 
						|
                        },
 | 
						|
                        accent: {
 | 
						|
                            DEFAULT: "hsl(var(--accent))",
 | 
						|
                            foreground: "hsl(var(--accent-foreground))",
 | 
						|
                        },
 | 
						|
                        popover: {
 | 
						|
                            DEFAULT: "hsl(var(--popover))",
 | 
						|
                            foreground: "hsl(var(--popover-foreground))",
 | 
						|
                        },
 | 
						|
                        card: {
 | 
						|
                            DEFAULT: "hsl(var(--card))",
 | 
						|
                            foreground: "hsl(var(--card-foreground))",
 | 
						|
                        },
 | 
						|
                    },
 | 
						|
                    borderRadius: {
 | 
						|
                        lg: "var(--radius)",
 | 
						|
                        md: "calc(var(--radius) - 2px)",
 | 
						|
                        sm: "calc(var(--radius) - 4px)",
 | 
						|
                    },
 | 
						|
                }
 | 
						|
            }
 | 
						|
        }
 | 
						|
    </script>
 | 
						|
    <style type="text/tailwindcss">
 | 
						|
        @layer base {
 | 
						|
            :root {
 | 
						|
                --background: 0 0% 100%;
 | 
						|
                --foreground: 222.2 84% 4.9%;
 | 
						|
                --muted: 210 40% 96.1%;
 | 
						|
                --muted-foreground: 215.4 16.3% 46.9%;
 | 
						|
                --popover: 0 0% 100%;
 | 
						|
                --popover-foreground: 222.2 84% 4.9%;
 | 
						|
                --card: 0 0% 100%;
 | 
						|
                --card-foreground: 222.2 84% 4.9%;
 | 
						|
                --border: 214.3 31.8% 91.4%;
 | 
						|
                --input: 214.3 31.8% 91.4%;
 | 
						|
                --primary: 222.2 47.4% 11.2%;
 | 
						|
                --primary-foreground: 210 40% 98%;
 | 
						|
                --secondary: 210 40% 96.1%;
 | 
						|
                --secondary-foreground: 222.2 47.4% 11.2%;
 | 
						|
                --accent: 210 40% 96.1%;
 | 
						|
                --accent-foreground: 222.2 47.4% 11.2%;
 | 
						|
                --destructive: 0 84.2% 60.2%;
 | 
						|
                --destructive-foreground: 210 40% 98%;
 | 
						|
                --ring: 215 20.2% 65.1%;
 | 
						|
                --radius: 0.5rem;
 | 
						|
            }
 | 
						|
            
 | 
						|
            .dark {
 | 
						|
                --background: 222.2 84% 4.9%;
 | 
						|
                --foreground: 210 40% 98%;
 | 
						|
                --muted: 217.2 32.6% 17.5%;
 | 
						|
                --muted-foreground: 215 20.2% 65.1%;
 | 
						|
                --popover: 222.2 84% 4.9%;
 | 
						|
                --popover-foreground: 210 40% 98%;
 | 
						|
                --card: 222.2 84% 4.9%;
 | 
						|
                --card-foreground: 210 40% 98%;
 | 
						|
                --border: 217.2 32.6% 17.5%;
 | 
						|
                --input: 217.2 32.6% 17.5%;
 | 
						|
                --primary: 210 40% 98%;
 | 
						|
                --primary-foreground: 222.2 47.4% 11.2%;
 | 
						|
                --secondary: 217.2 32.6% 17.5%;
 | 
						|
                --secondary-foreground: 210 40% 98%;
 | 
						|
                --accent: 217.2 32.6% 17.5%;
 | 
						|
                --accent-foreground: 210 40% 98%;
 | 
						|
                --destructive: 0 62.8% 30.6%;
 | 
						|
                --destructive-foreground: 210 40% 98%;
 | 
						|
                --ring: 212.7 26.8% 83.9%;
 | 
						|
            }
 | 
						|
        }
 | 
						|
        
 | 
						|
        @layer base {
 | 
						|
            * {
 | 
						|
                @apply border-border;
 | 
						|
            }
 | 
						|
            body {
 | 
						|
                @apply bg-background text-foreground;
 | 
						|
            }
 | 
						|
        }
 | 
						|
    </style>
 | 
						|
</head>
 | 
						|
<body class="min-h-screen bg-gradient-to-br from-slate-50 to-slate-100">
 | 
						|
    <div id="app" x-data="app()" x-cloak>
 | 
						|
        <!-- Login View -->
 | 
						|
        <template x-if="!state.token">
 | 
						|
            <div class="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-50 to-slate-100">
 | 
						|
                <div class="w-full max-w-md shadow-lg rounded-lg p-6 bg-white">
 | 
						|
                    <div class="text-center pb-4">
 | 
						|
                        <div class="mx-auto w-12 h-12 bg-slate-100 rounded-full flex items-center justify-center mb-4">
 | 
						|
                            <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-6 h-6 text-slate-600">
 | 
						|
                                <rect width="18" height="11" x="3" y="11" rx="2" ry="2"></rect>
 | 
						|
                                <path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
 | 
						|
                            </svg>
 | 
						|
                        </div>
 | 
						|
                        <h3 class="text-2xl font-semibold text-slate-800">Admin Login</h3>
 | 
						|
                    </div>
 | 
						|
                    <form @submit.prevent="handleLogin" class="space-y-6">
 | 
						|
                        <div class="space-y-2">
 | 
						|
                            <input 
 | 
						|
                                type="password" 
 | 
						|
                                placeholder="Enter admin password" 
 | 
						|
                                x-model="state.password"
 | 
						|
                                class="w-full px-4 py-3 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-slate-500 focus:border-transparent"
 | 
						|
                                :disabled="state.loading"
 | 
						|
                                required
 | 
						|
                            />
 | 
						|
                        </div>
 | 
						|
                        <button 
 | 
						|
                            type="submit" 
 | 
						|
                            class="w-full h-12 text-base bg-slate-900 text-white rounded-md hover:bg-slate-800 transition-colors disabled:opacity-50"
 | 
						|
                            :disabled="state.loading"
 | 
						|
                        >
 | 
						|
                            <span x-show="!state.loading">Sign In</span>
 | 
						|
                            <span x-show="state.loading" x-cloak>Signing in...</span>
 | 
						|
                        </button>
 | 
						|
                    </form>
 | 
						|
                    
 | 
						|
                    <div class="relative my-6">
 | 
						|
                        <div class="absolute inset-0 flex items-center">
 | 
						|
                            <div class="w-full border-t border-gray-300"></div>
 | 
						|
                        </div>
 | 
						|
                        <div class="relative flex justify-center text-sm">
 | 
						|
                            <span class="px-2 bg-white text-gray-500">Or continue with</span>
 | 
						|
                        </div>
 | 
						|
                    </div>
 | 
						|
                    
 | 
						|
                    <button
 | 
						|
                        @click="handleGiteaLogin"
 | 
						|
                        type="button"
 | 
						|
                        class="w-full h-12 text-base border border-gray-300 rounded-md hover:bg-gray-50 transition-colors flex items-center justify-center gap-2"
 | 
						|
                    >
 | 
						|
                        <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
 | 
						|
                            <path d="M15 22v-4a4.8 4.8 0 0 0-1-3.5c3 0 6-2 6-5.5.08-1.25-.27-2.48-1-3.5.28-1.15.28-2.35 0-3.5 0 0-1 0-3 1.5-2.64-.5-5.36-.5-8 0C6 2 5 2 5 2c-.3 1.15-.3 2.35 0 3.5A5.403 5.403 0 0 0 4 9c0 3.5 3 5.5 6 5.5-.39.49-.68 1.05-.85 1.65-.17.6-.22 1.23-.15 1.85v4"/>
 | 
						|
                            <path d="M9 18c-4.51 2-5-2-7-2"/>
 | 
						|
                        </svg>
 | 
						|
                        Login with Gitea
 | 
						|
                    </button>
 | 
						|
                </div>
 | 
						|
            </div>
 | 
						|
        </template>
 | 
						|
 | 
						|
        <!-- Dashboard View -->
 | 
						|
        <template x-if="state.token">
 | 
						|
            <div class="min-h-screen">
 | 
						|
                <!-- Header -->
 | 
						|
                <div class="bg-white shadow-sm border-b">
 | 
						|
                    <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
 | 
						|
                        <div class="flex justify-between items-center h-16">
 | 
						|
                            <h1 class="text-xl font-semibold text-slate-800">Admin Dashboard</h1>
 | 
						|
                            <button
 | 
						|
                                @click="handleLogout"
 | 
						|
                                class="flex items-center gap-2 border border-gray-300 rounded-md px-4 py-2 hover:bg-gray-50 transition-colors"
 | 
						|
                            >
 | 
						|
                                <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-4 h-4">
 | 
						|
                                    <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
 | 
						|
                                    <polyline points="16 17 21 12 16 7"></polyline>
 | 
						|
                                    <line x1="21" x2="9" y1="12" y2="12"></line>
 | 
						|
                                </svg>
 | 
						|
                                Logout
 | 
						|
                            </button>
 | 
						|
                        </div>
 | 
						|
                    </div>
 | 
						|
                </div>
 | 
						|
 | 
						|
                <!-- Main Content -->
 | 
						|
                <div class="flex items-center justify-center min-h-[calc(100vh-4rem)] p-8">
 | 
						|
                    <div class="max-w-2xl w-full shadow-lg rounded-lg p-8 bg-white">
 | 
						|
                        <template x-if="state.loading && state.mediaQueue.length === 0">
 | 
						|
                            <div class="flex flex-col items-center justify-center py-16">
 | 
						|
                                <div class="animate-spin rounded-full h-8 w-8 border-t-2 border-b-2 border-slate-600 mb-4"></div>
 | 
						|
                                <p class="text-slate-600">Loading media...</p>
 | 
						|
                            </div>
 | 
						|
                        </template>
 | 
						|
 | 
						|
                        <template x-if="!state.loading && state.currentMedia">
 | 
						|
                            <div class="space-y-6">
 | 
						|
                                <!-- Image -->
 | 
						|
                                <div class="flex justify-center">
 | 
						|
                                    <img
 | 
						|
                                        :src="state.currentMedia.preview_url"
 | 
						|
                                        alt="Media to review"
 | 
						|
                                        class="max-w-full max-h-96 object-contain"
 | 
						|
                                        @error="handleImageError"
 | 
						|
                                        x-cloak
 | 
						|
                                    />
 | 
						|
                                </div>
 | 
						|
 | 
						|
                                <!-- Media Info -->
 | 
						|
                                <div class="text-center text-sm text-slate-600">
 | 
						|
                                    <p>Media ID: <span x-text="state.currentMedia.id"></span></p>
 | 
						|
                                    <p>Post ID: <span x-text="state.currentMedia.PostID"></span></p>
 | 
						|
                                    <p class="text-xs text-slate-400 mt-1" x-show="state.mediaQueue.length > 1">
 | 
						|
                                        <span x-text="state.mediaQueue.length - 1"></span> more item(s) pre-loaded
 | 
						|
                                    </p>
 | 
						|
                                </div>
 | 
						|
 | 
						|
                                <!-- Status Message -->
 | 
						|
                                <div class="text-center" x-show="state.statusMessage" x-cloak>
 | 
						|
                                    <p class="text-lg font-medium text-slate-700" x-text="state.statusMessage"></p>
 | 
						|
                                </div>
 | 
						|
 | 
						|
                                <!-- Action Buttons -->
 | 
						|
                                <div class="flex gap-4 justify-center">
 | 
						|
                                    <button
 | 
						|
                                        @click="handleAction('reject')"
 | 
						|
                                        :disabled="state.isProcessing"
 | 
						|
                                        class="flex items-center gap-2 px-8 py-3 border border-transparent rounded-md text-white bg-red-600 hover:bg-red-700 disabled:opacity-50 transition-colors"
 | 
						|
                                    >
 | 
						|
                                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
 | 
						|
                                            <path d="M18 6 6 18"></path>
 | 
						|
                                            <path d="m6 6 12 12"></path>
 | 
						|
                                        </svg>
 | 
						|
                                        Reject
 | 
						|
                                    </button>
 | 
						|
                                    <button
 | 
						|
                                        @click="handleAction('approve')"
 | 
						|
                                        :disabled="state.isProcessing"
 | 
						|
                                        class="flex items-center gap-2 px-8 py-3 border border-transparent rounded-md text-white bg-green-600 hover:bg-green-700 disabled:opacity-50 transition-colors"
 | 
						|
                                    >
 | 
						|
                                        <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="w-5 h-5">
 | 
						|
                                            <polyline points="20 6 9 17 4 12"></polyline>
 | 
						|
                                        </svg>
 | 
						|
                                        Approve
 | 
						|
                                    </button>
 | 
						|
                                </div>
 | 
						|
                            </div>
 | 
						|
                        </template>
 | 
						|
 | 
						|
                        <template x-if="!state.currentMedia && !state.loading">
 | 
						|
                            <div class="text-center py-16">
 | 
						|
                                <p class="text-lg text-slate-600">No media available for review</p>
 | 
						|
                                <button
 | 
						|
                                    @click="fillMediaQueue"
 | 
						|
                                    class="mt-4 px-4 py-2 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
 | 
						|
                                >
 | 
						|
                                    Refresh
 | 
						|
                                </button>
 | 
						|
                            </div>
 | 
						|
                        </template>
 | 
						|
                    </div>
 | 
						|
                </div>
 | 
						|
            </div>
 | 
						|
        </template>
 | 
						|
    </div>
 | 
						|
 | 
						|
    <!-- Scripts -->
 | 
						|
    <script src="./js/app.js"></script>
 | 
						|
</body>
 | 
						|
</html> |