add oauth
Some checks are pending
Deploy ailog / build-and-deploy (push) Waiting to run

This commit is contained in:
2025-06-08 06:42:10 +09:00
parent c0e4dc63ea
commit 637028c264
65 changed files with 9101 additions and 804 deletions

View File

@ -44,7 +44,7 @@ comment_moderation = false
fs::write(path.join("config.toml"), config_content)?;
println!(" {} config.toml", "Created".cyan());
// Create default template
// Create modern template
let base_template = r#"<!DOCTYPE html>
<html lang="{{ config.language }}">
<head>
@ -54,18 +54,83 @@ comment_moderation = false
<link rel="stylesheet" href="/css/style.css">
</head>
<body>
<header>
<h1><a href="/">{{ config.title }}</a></h1>
<p>{{ config.description }}</p>
</header>
<div class="container">
<header class="main-header">
<div class="header-content">
<h1><a href="/" class="site-title">{{ config.title }}</a></h1>
<div class="header-actions">
<button class="ask-ai-btn" onclick="toggleAskAI()">
<span class="ai-icon">🤖</span>
Ask AI
</button>
</div>
</div>
</header>
<div class="ask-ai-panel" id="askAiPanel" style="display: none;">
<div class="ask-ai-content">
<h3>Hi! 👋</h3>
<p>I'm an AI assistant trained on this blog's content.</p>
<p>Ask me anything about the articles here.</p>
<div class="ask-ai-form">
<input type="text" id="aiQuestion" placeholder="What would you like to know?" />
<button onclick="askQuestion()">Ask</button>
</div>
<div id="aiResponse" class="ai-response"></div>
</div>
</div>
<main class="main-content">
{% block content %}{% endblock %}
</main>
{% block sidebar %}{% endblock %}
</div>
<main>
{% block content %}{% endblock %}
</main>
<footer>
<footer class="main-footer">
<p>&copy; 2025 {{ config.title }}</p>
</footer>
<script>
function toggleAskAI() {
const panel = document.getElementById('askAiPanel');
const isVisible = panel.style.display !== 'none';
panel.style.display = isVisible ? 'none' : 'block';
if (!isVisible) {
document.getElementById('aiQuestion').focus();
}
}
async function askQuestion() {
const question = document.getElementById('aiQuestion').value;
const responseDiv = document.getElementById('aiResponse');
if (!question.trim()) return;
responseDiv.innerHTML = '<div class="loading">Thinking...</div>';
try {
const response = await fetch('/api/ask', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ question: question })
});
const data = await response.json();
responseDiv.innerHTML = `<div class="ai-answer">${data.answer}</div>`;
} catch (error) {
responseDiv.innerHTML = '<div class="error">Sorry, I encountered an error. Please try again.</div>';
}
}
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
document.getElementById('askAiPanel').style.display = 'none';
}
});
</script>
</body>
</html>"#;
@ -75,15 +140,52 @@ comment_moderation = false
let index_template = r#"{% extends "base.html" %}
{% block content %}
<h2>Recent Posts</h2>
<ul class="post-list">
{% for post in posts %}
<li>
<a href="{{ post.url }}">{{ post.title }}</a>
<time>{{ post.date }}</time>
</li>
{% endfor %}
</ul>
<div class="timeline-container">
<div class="timeline-header">
<h2>Timeline</h2>
</div>
<div class="timeline-feed">
{% for post in posts %}
<article class="timeline-post">
<div class="post-header">
<div class="post-meta">
<time class="post-date">{{ post.date }}</time>
{% if post.language %}
<span class="post-lang">{{ post.language }}</span>
{% endif %}
</div>
</div>
<div class="post-content">
<h3 class="post-title">
<a href="{{ post.url }}">{{ post.title }}</a>
</h3>
{% if post.excerpt %}
<p class="post-excerpt">{{ post.excerpt }}</p>
{% endif %}
<div class="post-actions">
<a href="{{ post.url }}" class="read-more">Read more</a>
{% if post.markdown_url %}
<a href="{{ post.markdown_url }}" class="view-markdown" title="View Markdown">📝</a>
{% endif %}
{% if post.translation_url %}
<a href="{{ post.translation_url }}" class="view-translation" title="View Translation">🌐</a>
{% endif %}
</div>
</div>
</article>
{% endfor %}
</div>
{% if posts|length == 0 %}
<div class="empty-state">
<p>No posts yet. Start writing!</p>
</div>
{% endif %}
</div>
{% endblock %}"#;
fs::write(path.join("templates/index.html"), index_template)?;
@ -94,76 +196,624 @@ comment_moderation = false
{% block title %}{{ post.title }} - {{ config.title }}{% endblock %}
{% block content %}
<article>
<h1>{{ post.title }}</h1>
<time>{{ post.date }}</time>
<div class="content">
{{ post.content | safe }}
</div>
</article>
<div class="article-container">
<article class="article-content">
<header class="article-header">
<h1 class="article-title">{{ post.title }}</h1>
<div class="article-meta">
<time class="article-date">{{ post.date }}</time>
{% if post.language %}
<span class="article-lang">{{ post.language }}</span>
{% endif %}
</div>
<div class="article-actions">
{% if post.markdown_url %}
<a href="{{ post.markdown_url }}" class="action-btn markdown-btn" title="View Markdown">
📝 Markdown
</a>
{% endif %}
{% if post.translation_url %}
<a href="{{ post.translation_url }}" class="action-btn translation-btn" title="View Translation">
🌐 {% if post.language == 'ja' %}English{% else %}日本語{% endif %}
</a>
{% endif %}
</div>
</header>
<div class="article-body">
{{ post.content | safe }}
</div>
</article>
</div>
{% endblock %}
{% block sidebar %}
<aside class="article-sidebar">
<nav class="toc">
<h3>Contents</h3>
<div id="toc-content">
<!-- TOC will be generated by JavaScript -->
</div>
</nav>
</aside>
<script>
document.addEventListener('DOMContentLoaded', function() {
generateTableOfContents();
});
function generateTableOfContents() {
const tocContainer = document.getElementById('toc-content');
const headings = document.querySelectorAll('.article-body h1, .article-body h2, .article-body h3, .article-body h4, .article-body h5, .article-body h6');
if (headings.length === 0) {
tocContainer.innerHTML = '<p class="no-toc">No headings found</p>';
return;
}
const tocList = document.createElement('ul');
tocList.className = 'toc-list';
headings.forEach((heading, index) => {
const id = `heading-${index}`;
heading.id = id;
const listItem = document.createElement('li');
listItem.className = `toc-item toc-${heading.tagName.toLowerCase()}`;
const link = document.createElement('a');
link.href = `#${id}`;
link.textContent = heading.textContent;
link.className = 'toc-link';
// Smooth scroll behavior
link.addEventListener('click', function(e) {
e.preventDefault();
heading.scrollIntoView({ behavior: 'smooth' });
});
listItem.appendChild(link);
tocList.appendChild(listItem);
});
tocContainer.appendChild(tocList);
}
</script>
{% endblock %}"#;
fs::write(path.join("templates/post.html"), post_template)?;
println!(" {} templates/post.html", "Created".cyan());
// Create default CSS
let css_content = r#"body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
color: #333;
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
header {
margin-bottom: 40px;
border-bottom: 1px solid #eee;
padding-bottom: 20px;
}
header h1 {
// Create modern CSS
let css_content = r#"/* Base styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
header h1 a {
color: #333;
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #1f2328;
background-color: #ffffff;
font-size: 16px;
}
.container {
min-height: 100vh;
display: grid;
grid-template-rows: auto auto 1fr auto;
grid-template-areas:
"header"
"ask-ai"
"main"
"footer";
}
/* Header styles */
.main-header {
grid-area: header;
background: #ffffff;
border-bottom: 1px solid #d1d9e0;
padding: 16px 24px;
position: sticky;
top: 0;
z-index: 100;
}
.header-content {
max-width: 1200px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.site-title {
color: #1f2328;
text-decoration: none;
font-size: 20px;
font-weight: 600;
}
.post-list {
list-style: none;
.site-title:hover {
color: #0969da;
}
/* Ask AI styles */
.ask-ai-btn {
background: #0969da;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
transition: background-color 0.2s;
}
.ask-ai-btn:hover {
background: #0860ca;
}
.ai-icon {
font-size: 16px;
}
.ask-ai-panel {
grid-area: ask-ai;
background: #f6f8fa;
border-bottom: 1px solid #d1d9e0;
padding: 24px;
}
.ask-ai-content {
max-width: 1200px;
margin: 0 auto;
}
.ask-ai-content h3 {
color: #1f2328;
margin-bottom: 8px;
}
.ask-ai-content p {
color: #656d76;
margin-bottom: 16px;
}
.ask-ai-form {
display: flex;
gap: 12px;
margin-bottom: 16px;
}
.ask-ai-form input {
flex: 1;
padding: 8px 12px;
border: 1px solid #d1d9e0;
border-radius: 6px;
font-size: 14px;
}
.ask-ai-form button {
background: #0969da;
color: white;
border: none;
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-weight: 500;
}
.ai-response {
background: white;
border: 1px solid #d1d9e0;
border-radius: 6px;
padding: 16px;
margin-top: 16px;
min-height: 60px;
}
.loading {
color: #656d76;
font-style: italic;
}
.ai-answer {
color: #1f2328;
line-height: 1.5;
}
.error {
color: #d1242f;
}
/* Main content styles */
.main-content {
grid-area: main;
max-width: 1200px;
margin: 0 auto;
padding: 24px;
width: 100%;
}
/* Timeline styles */
.timeline-container {
max-width: 600px;
margin: 0 auto;
}
.timeline-header {
margin-bottom: 24px;
text-align: center;
}
.timeline-header h2 {
color: #1f2328;
font-size: 24px;
font-weight: 600;
}
.timeline-feed {
display: flex;
flex-direction: column;
gap: 24px;
}
.timeline-post {
background: #ffffff;
border: 1px solid #d1d9e0;
border-radius: 8px;
padding: 20px;
transition: box-shadow 0.2s;
}
.timeline-post:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.post-header {
margin-bottom: 12px;
}
.post-meta {
display: flex;
gap: 12px;
align-items: center;
}
.post-date {
color: #656d76;
font-size: 14px;
}
.post-lang {
background: #f6f8fa;
color: #656d76;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.post-title {
margin-bottom: 8px;
}
.post-title a {
color: #1f2328;
text-decoration: none;
font-size: 18px;
font-weight: 600;
}
.post-title a:hover {
color: #0969da;
}
.post-excerpt {
color: #656d76;
margin-bottom: 16px;
line-height: 1.5;
}
.post-actions {
display: flex;
gap: 16px;
align-items: center;
}
.read-more {
color: #0969da;
text-decoration: none;
font-size: 14px;
font-weight: 500;
}
.read-more:hover {
text-decoration: underline;
}
.view-markdown, .view-translation {
color: #656d76;
text-decoration: none;
font-size: 14px;
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s;
}
.view-markdown:hover, .view-translation:hover {
background: #f6f8fa;
}
.empty-state {
text-align: center;
padding: 40px 20px;
color: #656d76;
}
/* Article page styles */
.article-container {
display: grid;
grid-template-columns: 1fr 240px;
gap: 40px;
max-width: 1200px;
margin: 0 auto;
}
.article-content {
min-width: 0;
}
.article-header {
margin-bottom: 32px;
padding-bottom: 24px;
border-bottom: 1px solid #d1d9e0;
}
.article-title {
color: #1f2328;
font-size: 32px;
font-weight: 600;
margin-bottom: 16px;
line-height: 1.25;
}
.article-meta {
display: flex;
gap: 16px;
align-items: center;
margin-bottom: 16px;
}
.article-date {
color: #656d76;
font-size: 14px;
}
.article-lang {
background: #f6f8fa;
color: #656d76;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 500;
}
.article-actions {
display: flex;
gap: 12px;
}
.action-btn {
color: #0969da;
text-decoration: none;
font-size: 14px;
padding: 6px 12px;
border: 1px solid #d1d9e0;
border-radius: 6px;
transition: all 0.2s;
}
.action-btn:hover {
background: #f6f8fa;
border-color: #0969da;
}
/* Article content */
.article-body {
color: #1f2328;
line-height: 1.6;
}
.article-body h1,
.article-body h2,
.article-body h3,
.article-body h4,
.article-body h5,
.article-body h6 {
color: #1f2328;
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.article-body h1 { font-size: 32px; }
.article-body h2 { font-size: 24px; }
.article-body h3 { font-size: 20px; }
.article-body h4 { font-size: 16px; }
.article-body p {
margin-bottom: 16px;
}
.article-body ul,
.article-body ol {
margin-bottom: 16px;
padding-left: 24px;
}
.article-body li {
margin-bottom: 4px;
}
.article-body blockquote {
border-left: 4px solid #d1d9e0;
padding-left: 16px;
margin: 16px 0;
color: #656d76;
}
.article-body pre {
background: #f6f8fa;
border: 1px solid #d1d9e0;
border-radius: 6px;
padding: 16px;
overflow-x: auto;
margin: 16px 0;
font-size: 14px;
}
.article-body code {
background: #f6f8fa;
padding: 2px 4px;
border-radius: 4px;
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', monospace;
font-size: 14px;
}
.article-body pre code {
background: none;
padding: 0;
}
.post-list li {
margin-bottom: 15px;
/* Sidebar styles */
.article-sidebar {
position: sticky;
top: 100px;
height: fit-content;
}
.post-list time {
color: #666;
font-size: 0.9em;
margin-left: 10px;
.toc {
background: #f6f8fa;
border: 1px solid #d1d9e0;
border-radius: 8px;
padding: 16px;
}
article time {
color: #666;
.toc h3 {
color: #1f2328;
font-size: 16px;
font-weight: 600;
margin-bottom: 12px;
}
.toc-list {
list-style: none;
}
.toc-item {
margin-bottom: 8px;
}
.toc-link {
color: #656d76;
text-decoration: none;
font-size: 14px;
line-height: 1.4;
display: block;
margin-bottom: 20px;
padding: 4px 0;
transition: color 0.2s;
}
pre {
background-color: #f4f4f4;
padding: 15px;
border-radius: 5px;
overflow-x: auto;
.toc-link:hover {
color: #0969da;
}
code {
background-color: #f4f4f4;
padding: 2px 5px;
border-radius: 3px;
font-family: 'Consolas', 'Monaco', monospace;
.toc-h1 { padding-left: 0; }
.toc-h2 { padding-left: 12px; }
.toc-h3 { padding-left: 24px; }
.toc-h4 { padding-left: 36px; }
.toc-h5 { padding-left: 48px; }
.toc-h6 { padding-left: 60px; }
.no-toc {
color: #656d76;
font-size: 14px;
font-style: italic;
}
/* Footer styles */
.main-footer {
grid-area: footer;
background: #f6f8fa;
border-top: 1px solid #d1d9e0;
padding: 24px;
text-align: center;
}
.main-footer p {
color: #656d76;
font-size: 14px;
}
/* Responsive design */
@media (max-width: 1024px) {
.article-container {
grid-template-columns: 1fr;
gap: 24px;
}
.article-sidebar {
position: static;
order: -1;
}
}
@media (max-width: 768px) {
.main-header {
padding: 12px 16px;
}
.header-content {
gap: 16px;
}
.ask-ai-panel {
padding: 16px;
}
.ask-ai-form {
flex-direction: column;
}
.timeline-container {
max-width: 100%;
}
.timeline-post {
padding: 16px;
}
.article-title {
font-size: 24px;
}
.article-actions {
flex-wrap: wrap;
}
.main-content {
padding: 16px;
}
}"#;
fs::write(path.join("static/css/style.css"), css_content)?;
@ -208,9 +858,14 @@ Happy blogging!"#;
println!("\n{}", "Blog initialized successfully!".green().bold());
println!("\nNext steps:");
println!(" 1. cd {}", path.display());
println!(" 2. ailog build");
println!(" 3. ailog serve");
println!(" 1. {} {}", "cd".yellow(), path.display());
println!(" 2. {} build", "ailog".yellow());
println!(" 3. {} serve", "ailog".yellow());
println!("\nOr use path as argument:");
println!(" {} -- build {}", "cargo run".yellow(), path.display());
println!(" {} -- serve {}", "cargo run".yellow(), path.display());
println!("\nTo create a new post:");
println!(" {} -- new \"Post Title\" {}", "cargo run".yellow(), path.display());
Ok(())
}