Initial commit

This commit is contained in:
2026-01-18 21:01:43 +03:00
commit 455b6df2ba
3329 changed files with 1143638 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
%sveltekit.head%
</head>
<body data-sveltekit-preload-data="hover">
<div style="display: contents">%sveltekit.body%</div>
</body>
</html>
+139
View File
@@ -0,0 +1,139 @@
<script>
import { goto } from '$app/navigation';
export let post;
export let onEdit;
export let onDelete;
function handleEdit() {
if (onEdit) onEdit(post);
}
function handleDelete() {
if (onDelete) onDelete(post);
}
// Format date for display
function formatDate(dateString) {
if (!dateString) return '';
const date = new Date(dateString);
return date.toLocaleString();
}
</script>
<div class="post-card">
<div class="post-header">
<h3 class="post-title">{post.title}</h3>
<span class={`status status-${post.status}`}>{post.status}</span>
</div>
<div class="post-content">
<p>{@html post.content.substring(0, 200) + (post.content.length > 200 ? '...' : '')}</p>
</div>
<div class="post-meta">
{#if post.scheduled_time}
<div>Scheduled: {formatDate(post.scheduled_time)}</div>
{/if}
<div>Created: {formatDate(post.created_at)}</div>
{#if post.updated_at}
<div>Updated: {formatDate(post.updated_at)}</div>
{/if}
</div>
<div class="post-actions">
<button on:click={handleEdit} class="btn-edit">Edit</button>
<button on:click={handleDelete} class="btn-delete">Delete</button>
</div>
</div>
<style>
.post-card {
border: 1px solid #ddd;
border-radius: 8px;
padding: 1rem;
margin-bottom: 1rem;
background-color: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.post-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 0.5rem;
}
.post-title {
margin: 0;
font-size: 1.2rem;
color: #333;
}
.status {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.8rem;
font-weight: bold;
text-transform: uppercase;
}
.status-draft {
background-color: #ffc107;
color: #856404;
}
.status-scheduled {
background-color: #17a2b8;
color: white;
}
.status-published {
background-color: #28a745;
color: white;
}
.status-cancelled {
background-color: #dc3545;
color: white;
}
.post-content {
margin-bottom: 1rem;
color: #555;
line-height: 1.5;
}
.post-meta {
font-size: 0.85rem;
color: #666;
margin-bottom: 1rem;
}
.post-meta div {
margin-bottom: 0.25rem;
}
.post-actions {
display: flex;
gap: 0.5rem;
}
.post-actions button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 0.9rem;
}
.btn-edit {
background-color: #007bff;
color: white;
}
.btn-delete {
background-color: #dc3545;
color: white;
}
</style>
@@ -0,0 +1,272 @@
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
export let postId = null;
export let onSave = () => {};
let title = '';
let content = '';
let scheduledTime = '';
let status = 'draft';
let loading = false;
let error = null;
// Load post data if editing
onMount(async () => {
if (postId) {
loadPost();
}
});
async function loadPost() {
try {
const token = localStorage.getItem('token');
if (!token) {
throw new Error('No authentication token found');
}
const response = await fetch(`${import.meta.env.VITE_API_URL || '/api/v1'}/posts/${postId}`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
if (response.status === 401) {
localStorage.removeItem('token');
goto('/login');
return;
}
throw new Error(`Failed to load post: ${response.statusText}`);
}
const postData = await response.json();
title = postData.title || '';
content = postData.content || '';
status = postData.status || 'draft';
// Convert scheduled time to local format if exists
if (postData.scheduled_time) {
const date = new Date(postData.scheduled_time);
scheduledTime = date.toISOString().slice(0, 16);
}
} catch (err) {
console.error('Error loading post:', err);
error = err.message;
}
}
async function savePost() {
if (!title.trim() || !content.trim()) {
error = 'Title and content are required';
return;
}
loading = true;
error = null;
try {
const token = localStorage.getItem('token');
if (!token) {
throw new Error('No authentication token found');
}
const postData = {
title: title.trim(),
content: content.trim(),
status,
};
if (scheduledTime) {
postData.scheduled_time = scheduledTime;
}
let response;
if (postId) {
// Update existing post
response = await fetch(`${import.meta.env.VITE_API_URL || '/api/v1'}/posts/${postId}`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(postData)
});
} else {
// Create new post
response = await fetch(`${import.meta.env.VITE_API_URL || '/api/v1'}/posts/`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(postData)
});
}
if (!response.ok) {
if (response.status === 401) {
localStorage.removeItem('token');
goto('/login');
return;
}
const errorData = await response.json();
throw new Error(errorData.detail || 'Failed to save post');
}
const savedPost = await response.json();
onSave(savedPost);
} catch (err) {
console.error('Error saving post:', err);
error = err.message;
} finally {
loading = false;
}
}
// Handle form submission
async function handleSubmit(e) {
e.preventDefault();
await savePost();
}
</script>
<div class="post-editor">
<form on:submit={handleSubmit}>
{#if error}
<div class="error-message">
{error}
</div>
{/if}
<div class="form-group">
<label for="title">Title:</label>
<input
id="title"
type="text"
bind:value={title}
placeholder="Enter post title"
disabled={loading}
required
/>
</div>
<div class="form-group">
<label for="content">Content:</label>
<textarea
id="content"
bind:value={content}
placeholder="Write your post content here..."
rows="10"
disabled={loading}
required
></textarea>
</div>
<div class="form-group">
<label for="status">Status:</label>
<select id="status" bind:value={status} disabled={loading}>
<option value="draft">Draft</option>
<option value="scheduled">Scheduled</option>
<option value="published">Published</option>
<option value="cancelled">Cancelled</option>
</select>
</div>
<div class="form-group">
<label for="scheduled_time">Scheduled Time:</label>
<input
id="scheduled_time"
type="datetime-local"
bind:value={scheduledTime}
disabled={loading}
/>
</div>
<div class="button-group">
<button type="submit" disabled={loading}>
{loading ? 'Saving...' : postId ? 'Update Post' : 'Create Post'}
</button>
<button type="button" on:click={() => goto('/posts')} disabled={loading}>
Cancel
</button>
</div>
</form>
</div>
<style>
.post-editor {
max-width: 800px;
margin: 0 auto;
padding: 1rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
margin-bottom: 0.5rem;
font-weight: bold;
}
.form-group input,
.form-group textarea,
.form-group select {
width: 100%;
padding: 0.5rem;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
.form-group textarea {
resize: vertical;
}
.button-group {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
button {
padding: 0.5rem 1rem;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
button[type="submit"] {
background-color: #007bff;
color: white;
}
button[type="submit"]:disabled {
background-color: #6c757d;
cursor: not-allowed;
}
button[type="button"] {
background-color: #6c757d;
color: white;
}
button[type="button"]:disabled {
background-color: #adb5bd;
cursor: not-allowed;
}
.error-message {
background-color: #f8d7da;
color: #721c24;
padding: 0.75rem;
border: 1px solid #f5c6cb;
border-radius: 4px;
margin-bottom: 1rem;
}
</style>
+73
View File
@@ -0,0 +1,73 @@
import { writable, derived } from 'svelte/store';
// Main auth store
export const auth = writable({
isAuthenticated: false,
user: null,
token: null
});
// Helper functions
export const login = (userData, accessToken) => {
const user = {
...userData,
isAuthenticated: true
};
// Store token in localStorage
if (typeof window !== 'undefined') {
localStorage.setItem('access_token', accessToken);
}
auth.set({
isAuthenticated: true,
user: user,
token: accessToken
});
};
export const logout = () => {
// Remove token from localStorage
if (typeof window !== 'undefined') {
localStorage.removeItem('access_token');
}
auth.set({
isAuthenticated: false,
user: null,
token: null
});
};
export const initializeAuth = () => {
if (typeof window !== 'undefined') {
const token = localStorage.getItem('access_token');
if (token) {
// Here we would normally verify the token with an API call
// For now, we'll just set the auth state based on the stored token
auth.set({
isAuthenticated: true,
user: null, // We would fetch user data based on the token
token: token
});
}
}
};
// Derived store for easy access to auth status
export const isAuthenticated = derived(
auth,
$auth => $auth.isAuthenticated
);
// Derived store for easy access to user
export const currentUser = derived(
auth,
$auth => $auth.user
);
// Derived store for easy access to token
export const authToken = derived(
auth,
$auth => $auth.token
);
+51
View File
@@ -0,0 +1,51 @@
// Utility functions for authentication checks
/**
* Check if user is authenticated by checking for token in localStorage
* @returns {boolean} - True if user is authenticated, false otherwise
*/
export function isAuthenticated() {
const token = localStorage.getItem('token');
return !!token;
}
/**
* Get the current user's token
* @returns {string|null} - Token if exists, null otherwise
*/
export function getToken() {
return localStorage.getItem('token');
}
/**
* Clear authentication data
*/
export function logout() {
localStorage.removeItem('token');
}
/**
* Validate token by making a request to the backend
* @returns {Promise<boolean>} - Promise that resolves to true if token is valid, false otherwise
*/
export async function validateToken() {
const token = getToken();
if (!token) {
return false;
}
try {
const response = await fetch('/api/v1/auth/me', {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
return response.ok;
} catch (error) {
console.error('Error validating token:', error);
return false;
}
}
+97
View File
@@ -0,0 +1,97 @@
<script>
import { onMount } from 'svelte';
import { browser } from '$app/environment';
// This will hold our callback function
let telegramCallback;
onMount(async () => {
if (browser) {
// Load Telegram Widget script dynamically
const script = document.createElement('script');
script.src = 'https://telegram.org/js/telegram-widget.js?22';
script.async = true;
document.head.appendChild(script);
// Define the callback function
telegramCallback = (user) => {
console.log('Telegram user data:', user);
// Send the user data to our backend
fetch(`${import.meta.env.VITE_API_URL || '/api/v1'}/auth/telegram-login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
id: user.id,
first_name: user.first_name,
last_name: user.last_name,
username: user.username,
photo_url: user.photo_url,
auth_date: user.auth_date,
hash: user.hash
})
})
.then(response => response.json())
.then(data => {
console.log('Login response:', data);
// Store the token in localStorage or a cookie
if (data.access_token) {
localStorage.setItem('access_token', data.access_token);
// Redirect to dashboard
window.location.href = '/dashboard';
}
})
.catch(error => {
console.error('Error logging in:', error);
});
};
// Add the callback function to window object so Telegram widget can access it
window.telegramLoginCallback = telegramCallback;
}
});
// Cleanup function
function onDestroy() {
if (window.telegramLoginCallback) {
delete window.telegramLoginCallback;
}
}
</script>
<main class="login-container">
<h1>Вход в TelePost Clone</h1>
<p>Используйте Telegram для входа в систему</p>
<!-- Telegram Login Button -->
<div
class="telegram-login"
data-telegram-login="YOUR_BOT_NAME"
data-size="large"
data-auth-url={`${import.meta.env.VITE_API_URL || '/api/v1'}/auth/telegram-login`}
data-request-access="write"
data-onauth="window.telegramLoginCallback(user)">
</div>
<style>
.login-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
padding: 1rem;
}
h1 {
margin-bottom: 0.5rem;
color: #333;
}
p {
margin-bottom: 2rem;
color: #666;
}
+63
View File
@@ -0,0 +1,63 @@
<script>
import { browser } from '$app/environment';
import { goto, page } from '$app/stores';
import { onMount } from 'svelte';
import { isAuthenticated, validateToken } from '$lib/utils/auth.js';
let loading = true;
let authorized = false;
onMount(async () => {
// Check authentication only in browser
if (browser) {
const authenticated = isAuthenticated();
if (!authenticated) {
// Redirect to login if not authenticated
goto('/login');
return;
}
// Optionally validate token with backend
const tokenValid = await validateToken();
if (!tokenValid) {
// If token is invalid, clear it and redirect to login
localStorage.removeItem('token');
goto('/login');
return;
}
authorized = true;
}
loading = false;
});
</script>
{#if loading}
<div class="loading">Checking authentication...</div>
{:else}
{#if authorized}
<slot />
{:else}
<div class="unauthorized">Access denied. Redirecting...</div>
{/if}
{/if}
<style>
.loading {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-size: 1.2rem;
color: #666;
}
.unauthorized {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
font-size: 1.2rem;
color: #dc3545;
}
</style>
+198
View File
@@ -0,0 +1,198 @@
<script>
import { onMount } from 'svelte';
import { goto } from '$app/navigation';
import PostCard from '$lib/components/PostCard.svelte';
let posts = [];
let loading = true;
let error = null;
onMount(async () => {
loadPosts();
});
async function loadPosts() {
try {
const token = localStorage.getItem('token');
if (!token) {
goto('/login');
return;
}
const response = await fetch(`${import.meta.env.VITE_API_URL || '/api/v1'}/posts/`, {
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
if (response.status === 401) {
localStorage.removeItem('token');
goto('/login');
return;
}
throw new Error(`Failed to load posts: ${response.statusText}`);
}
posts = await response.json();
} catch (err) {
console.error('Error loading posts:', err);
error = err.message;
} finally {
loading = false;
}
}
async function handleEdit(post) {
goto(`/posts/edit/${post.id}`);
}
async function handleDelete(post) {
if (!confirm(`Are you sure you want to delete "${post.title}"?`)) {
return;
}
try {
const token = localStorage.getItem('token');
if (!token) {
throw new Error('No authentication token found');
}
const response = await fetch(`/api/v1/posts/${post.id}`, {
method: 'DELETE',
headers: {
'Authorization': `Bearer ${token}`,
'Content-Type': 'application/json'
}
});
if (!response.ok) {
if (response.status === 401) {
localStorage.removeItem('token');
goto('/login');
return;
}
throw new Error(`Failed to delete post: ${response.statusText}`);
}
// Remove the post from the local array
posts = posts.filter(p => p.id !== post.id);
} catch (err) {
console.error('Error deleting post:', err);
alert(err.message);
}
}
</script>
<div class="posts-page">
<div class="header">
<h1>My Posts</h1>
<button on:click={() => goto('/posts/create')} class="btn-create">
Create New Post
</button>
</div>
{#if error}
<div class="error-message">
{error}
</div>
{/if}
{#if loading}
<div class="loading">
Loading posts...
</div>
{:else}
{#if posts.length === 0}
<div class="empty-state">
<p>You don't have any posts yet.</p>
<button on:click={() => goto('/posts/create')} class="btn-primary">
Create your first post
</button>
</div>
{:else}
<div class="posts-list">
{#each posts as post (post.id)}
<PostCard
{post}
on:edit={handleEdit}
on:delete={handleDelete}
/>
{/each}
</div>
{/if}
{/if}
</div>
<style>
.posts-page {
max-width: 1200px;
margin: 0 auto;
padding: 1rem;
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.header h1 {
margin: 0;
color: #333;
}
.btn-create {
padding: 0.5rem 1rem;
background-color: #28a745;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.btn-primary {
padding: 0.75rem 1.5rem;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 1rem;
}
.loading {
text-align: center;
padding: 2rem;
font-size: 1.2rem;
color: #666;
}
.error-message {
background-color: #f8d7da;
color: #721c24;
padding: 0.75rem;
border: 1px solid #f5c6cb;
border-radius: 4px;
margin-bottom: 1rem;
}
.empty-state {
text-align: center;
padding: 3rem;
color: #666;
}
.empty-state p {
margin-bottom: 1rem;
font-size: 1.2rem;
}
.posts-list {
display: grid;
gap: 1rem;
}
</style>
@@ -0,0 +1,27 @@
<script>
import { goto } from '$app/navigation';
import PostEditor from '$lib/components/PostEditor.svelte';
async function handleSave(post) {
// Redirect to the posts list after saving
goto('/posts');
}
</script>
<div class="create-post-page">
<h1>Create New Post</h1>
<PostEditor onSave={handleSave} />
</div>
<style>
.create-post-page {
max-width: 800px;
margin: 0 auto;
padding: 1rem;
}
.create-post-page h1 {
margin-bottom: 1rem;
color: #333;
}
</style>
@@ -0,0 +1,30 @@
<script>
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import PostEditor from '$lib/components/PostEditor.svelte';
let postId = $page.params.id;
async function handleSave(post) {
// Redirect to the posts list after saving
goto('/posts');
}
</script>
<div class="edit-post-page">
<h1>Edit Post</h1>
<PostEditor {postId} onSave={handleSave} />
</div>
<style>
.edit-post-page {
max-width: 800px;
margin: 0 auto;
padding: 1rem;
}
.edit-post-page h1 {
margin-bottom: 1rem;
color: #333;
}
</style>