s4s-home / view.php
soiz1's picture
Upload 14 files
240bcc5 verified
<?php
$logFile = __DIR__ . '/logs.txt';
if (!file_exists($logFile)) {
file_put_contents($logFile, '');
chmod($logFile, 0666); // 読み書き可能な権限を設定
}
ini_set('log_errors', 1);
ini_set('error_log', $logFile);
ini_set('display_errors', 0);
error_reporting(E_ALL); // すべてのエラーを報告
// その他の設定
require_once 'config.php';
header('Content-Type: text/html; charset=UTF-8');
mb_internal_encoding('UTF-8');
session_start();
// Googleログイン確認
if (!isset($_SESSION['user_id'])) {
header('Location: login.php');
exit;
}
$project_id = $_GET['id'] ?? 0;
// データベース接続
$conn = new mysqli($host, $username, $password, $dbname);
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
if (!$conn->set_charset("utf8mb4")) {
throw new Exception("文字コード設定失敗: " . $conn->error);
}
// プロジェクト情報取得
$stmt = $conn->prepare("SELECT p.*, u.display_name as author_display_name, u.icon_url as author_icon_url
FROM projects p
LEFT JOIN user_data u ON p.author_id = u.google_id
WHERE p.id = ?");
$stmt->bind_param("i", $project_id);
$stmt->execute();
$result = $stmt->get_result();
$project = $result->fetch_assoc();
if (!$project) {
die("プロジェクトが見つかりません");
}
// パスワードチェック
$password_required = !empty($project['password']);
$password_verified = false;
if ($password_required) {
if (isset($_POST['project_password'])) {
if ($_POST['project_password'] === $project['password']) {
$_SESSION['verified_projects'][$project_id] = true;
$password_verified = true;
} else {
$password_error = "パスワードが間違っています";
}
} elseif (isset($_SESSION['verified_projects'][$project_id])) {
$password_verified = true;
}
} else {
$password_verified = true;
}
if (!$password_verified) {
// パスワード入力フォームを表示して終了
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>パスワードが必要です - Scratch School</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet">
<style>
body {
font-family: 'Noto Sans JP', sans-serif;
background-color: #f5f5f5;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
}
.password-form {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
width: 100%;
max-width: 400px;
text-align: center;
}
.password-form h1 {
margin-bottom: 20px;
font-size: 24px;
}
.password-form input {
width: 100%;
padding: 10px;
margin-bottom: 20px;
border: 1px solid #ddd;
border-radius: 5px;
font-size: 16px;
}
.password-form button {
background-color: #4d97ff;
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
.error {
color: #ff4444;
margin-bottom: 20px;
}
</style>
</head>
<body>
<div class="password-form">
<h1>このプロジェクトはパスワードで保護されています</h1>
<?php if (isset($password_error)): ?>
<div class="error"><?php echo htmlspecialchars($password_error); ?></div>
<?php endif; ?>
<form method="POST">
<input type="password" name="project_password" placeholder="パスワードを入力" required>
<button type="submit">送信</button>
</form>
</div>
</body>
</html>
<?php
exit;
}
// 閲覧数更新
$conn->query("UPDATE projects SET views = views + 1 WHERE id = $project_id");
// ユーザーが星を付けたかどうか確認
$has_starred = false;
$starred_users = json_decode($project['starred_users'] ?? '[]', true) ?? [];
if (isset($_SESSION['user_id'])) {
$has_starred = in_array($_SESSION['user_id'], $starred_users);
}
// 星のトグル処理
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['toggle_star'])) {
if ($has_starred) {
// 星を削除
$starred_users = array_diff($starred_users, [$_SESSION['user_id']]);
$starred_users_json = $conn->real_escape_string(json_encode(array_values($starred_users)));
$conn->query("UPDATE projects SET stars = stars - 1, starred_users = '$starred_users_json' WHERE id = $project_id");
$project['stars']--;
$has_starred = false;
} else {
// 星を追加
$starred_users[] = $_SESSION['user_id'];
$starred_users_json = $conn->real_escape_string(json_encode(array_values($starred_users)));
$conn->query("UPDATE projects SET stars = stars + 1, starred_users = '$starred_users_json' WHERE id = $project_id");
$project['stars']++;
$has_starred = true;
}
}
// コメント処理
$comment_error = null;
$comment_success = null;
$comments = [];
// commentカラムのJSONデータをデコード
$comment_data = json_decode($project['comment'], true) ?? ['can_comment' => false, 'history' => []];
$can_comment = $comment_data['can_comment'] ?? false;
$comments = $comment_data['history'] ?? [];
if ($can_comment) {
// コメント送信処理
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['comment_content'])) {
$content = trim($_POST['comment_content']);
if (empty($content)) {
$comment_error = "コメント内容を入力してください";
} elseif (strlen($content) > 1000) {
$comment_error = "コメントは1000文字以内で入力してください";
} else {
// ユーザー情報を取得
$stmt = $conn->prepare("SELECT display_name, icon_url FROM user_data WHERE google_id = ?");
$stmt->bind_param("s", $_SESSION['user_id']);
$stmt->execute();
$user_result = $stmt->get_result();
$user_data = $user_result->fetch_assoc();
if ($user_data) {
$new_comment = [
'user_id' => $_SESSION['user_id'],
'name' => $user_data['display_name'],
'icon_url' => $user_data['icon_url'],
'content' => $content,
'timestamp' => time()
];
array_unshift($comments, $new_comment);
$comment_data['history'] = $comments;
// データベース更新
$json_data = json_encode($comment_data, JSON_UNESCAPED_UNICODE);
$stmt = $conn->prepare("UPDATE projects SET comment = ? WHERE id = ?");
$stmt->bind_param("si", $json_data, $project_id);
if ($stmt->execute()) {
$comment_success = "コメントを投稿しました";
} else {
$comment_error = "コメントの保存に失敗しました";
}
} else {
$comment_error = "ユーザー情報が見つかりません";
}
}
}
// コメント削除/編集処理
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['comment_action'])) {
$index = $_POST['comment_index'] ?? null;
$is_owner = ($_SESSION['user_id'] === $project['author_id']);
if ($index !== null && isset($comments[$index])) {
$comment = $comments[$index];
$is_comment_owner = ($comment['user_id'] === $_SESSION['user_id']);
if ($is_owner || $is_comment_owner) {
if ($_POST['comment_action'] === 'delete') {
// コメント削除
array_splice($comments, $index, 1);
$comment_data['history'] = $comments;
$json_data = json_encode($comment_data, JSON_UNESCAPED_UNICODE);
$stmt = $conn->prepare("UPDATE projects SET comment = ? WHERE id = ?");
$stmt->bind_param("si", $json_data, $project_id);
$stmt->execute();
} elseif ($_POST['comment_action'] === 'edit' && isset($_POST['edited_content'])) {
// コメント編集
$edited_content = trim($_POST['edited_content']);
if (!empty($edited_content) && strlen($edited_content) <= 1000) {
$comments[$index]['content'] = $edited_content;
$comments[$index]['edited'] = true;
$comment_data['history'] = $comments;
$json_data = json_encode($comment_data, JSON_UNESCAPED_UNICODE);
$stmt = $conn->prepare("UPDATE projects SET comment = ? WHERE id = ?");
$stmt->bind_param("si", $json_data, $project_id);
$stmt->execute();
}
}
}
}
}
}
// 現在のユーザー情報を取得
$current_user_stmt = $conn->prepare("SELECT display_name, icon_url FROM user_data WHERE google_id = ?");
$current_user_stmt->bind_param("s", $_SESSION['user_id']);
$current_user_stmt->execute();
$current_user_result = $current_user_stmt->get_result();
$current_user = $current_user_result->fetch_assoc();
// コメント投稿者のアイコン情報を取得
$comment_user_ids = array_column($comments, 'user_id');
if (!empty($comment_user_ids)) {
$placeholders = implode(',', array_fill(0, count($comment_user_ids), '?'));
$types = str_repeat('s', count($comment_user_ids));
$stmt = $conn->prepare("SELECT google_id, icon_url FROM user_data WHERE google_id IN ($placeholders)");
$stmt->bind_param($types, ...$comment_user_ids);
$stmt->execute();
$icon_result = $stmt->get_result();
$user_icons = [];
while ($row = $icon_result->fetch_assoc()) {
$user_icons[$row['google_id']] = $row['icon_url'];
}
// コメントデータにアイコンURLを追加
foreach ($comments as &$comment) {
if (isset($user_icons[$comment['user_id']])) {
$comment['icon_url'] = $user_icons[$comment['user_id']];
}
}
}
$conn->close();
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-PJXRKX3F');</script>
<!-- End Google Tag Manager -->
<title><?php echo htmlspecialchars($project['project_name']); ?> - Scratch School</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@400;500;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
:root {
--primary-color: #4d97ff;
--secondary-color: #575e75;
--background-color: #f5f5f5;
--card-bg: #ffffff;
--text-color: #333333;
--error-color: #ff4444;
--success-color: #00c851;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Noto Sans JP', sans-serif;
background-color: var(--background-color);
color: var(--text-color);
line-height: 1.6;
padding: 20px;
}
.container {
max-width: 98%;
margin: 0 auto;
background-color: var(--card-bg);
border-radius: 10px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
padding: 30px;
}
.project-header {
display: flex;
margin-bottom: 20px;
gap: 20px;
align-items: center;
}
.project-thumbnail {
width: 200px;
height: 150px;
background-color: #f0f0f0;
border-radius: 8px;
overflow: hidden;
flex-shrink: 0;
position: relative;
}
.project-thumbnail img {
width: 100%;
height: 100%;
object-fit: cover;
}
.project-meta {
flex-grow: 1;
}
.project-title-container {
display: flex;
align-items: center;
gap: 15px;
margin-bottom: 10px;
flex-wrap: wrap;
}
.project-title {
font-size: 24px;
margin: 0;
}
.project-author {
color: var(--secondary-color);
margin-bottom: 10px;
display: flex;
align-items: center;
gap: 8px;
}
.author-icon {
width: 24px;
height: 24px;
border-radius: 50%;
object-fit: cover;
}
.project-stats {
display: flex;
gap: 20px;
margin-bottom: 10px;
}
.star-button {
background: none;
border: none;
cursor: pointer;
font-size: 24px;
color: <?php echo $has_starred ? '#ffcc00' : '#ccc'; ?>;
padding: 0;
}
.project-content {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.project-iframe-container {
flex: 2;
}
.project-description-container {
flex: 1;
overflow-y: auto;
max-height: 600px;
padding: 10px;
border: 1px solid #eee;
border-radius: 8px;
}
.project-description {
white-space: pre-wrap;
}
.markdown-preview {
border: 1px solid #ddd;
padding: 10px;
border-radius: 5px;
margin-bottom: 10px;
background-color: #f9f9f9;
}
.project-iframe {
width: 100%;
height: 600px;
border: none;
border-radius: 8px;
background-color: #f0f0f0;
}
.loading-thumbnail {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
color: #999;
}
.action-button {
background-color: var(--primary-color);
color: white;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
text-decoration: none;
font-size: 14px;
}
/* コメントセクション */
.comments-section {
margin-top: 40px;
border-top: 1px solid #eee;
padding-top: 20px;
}
.comments-title {
font-size: 20px;
margin-bottom: 20px;
}
.comment-form {
margin-bottom: 30px;
}
.comment-textarea {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 5px;
min-height: 100px;
margin-bottom: 10px;
font-family: inherit;
}
.comment-submit {
background-color: var(--primary-color);
color: white;
border: none;
padding: 8px 16px;
border-radius: 5px;
cursor: pointer;
}
.comment-preview {
margin-top: 10px;
padding: 10px;
border: 1px dashed #ccc;
border-radius: 5px;
background-color: #f9f9f9;
display: none;
}
.comment-list {
display: flex;
flex-direction: column;
gap: 20px;
}
.comment-item {
border: 1px solid #eee;
border-radius: 8px;
padding: 15px;
position: relative;
}
.comment-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.comment-user-icon {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
}
.comment-user-name {
font-weight: 500;
}
.comment-time {
color: #777;
font-size: 12px;
margin-left: auto;
}
.comment-content {
margin-left: 42px;
}
.comment-actions {
margin-top: 10px;
display: flex;
gap: 10px;
justify-content: flex-end;
}
.comment-action-button {
background: none;
border: none;
color: #777;
cursor: pointer;
font-size: 12px;
}
.comment-action-button:hover {
color: var(--primary-color);
}
.comment-edit-form {
display: none;
margin-top: 10px;
}
.error-message {
color: var(--error-color);
margin-bottom: 10px;
}
.success-message {
color: var(--success-color);
margin-bottom: 10px;
}
.preview-toggle {
background: none;
border: none;
color: #777;
cursor: pointer;
font-size: 13px;
margin-bottom: 10px;
}
</style>
</head>
<body>
<!-- Google Tag Manager (noscript) -->
<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=GTM-PJXRKX3F"
height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
<!-- End Google Tag Manager (noscript) -->
<div class="container">
<div class="project-header">
<div class="project-thumbnail" id="project-thumbnail" data-drive-id="<?php echo htmlspecialchars($project['drive_id']); ?>">
<div class="loading-thumbnail">読み込み中...</div>
<!-- 画像はJavaScriptで動的に挿入されます -->
</div>
<div class="project-meta">
<div class="project-title-container">
<h1 class="project-title"><?php echo htmlspecialchars($project['project_name']); ?></h1>
<a href="https://soiz1-soiz1-s4s-editor.hf.space/editor.html?project_url=https://drive-proxy-s4s.vercel.app/%3Ffile_id%3D<?php echo urlencode($project['drive_id']); ?>"
class="action-button" target="_blank">エディターで開く</a>
</div>
<p class="project-author">
<?php if ($project['author_icon_url']): ?>
<img src="<?php echo htmlspecialchars($project['author_icon_url']); ?>" class="author-icon" alt="作者のアイコン">
<?php endif; ?>
by <?php echo htmlspecialchars($project['author_display_name']); ?>
</p>
<div class="project-stats">
<form method="POST">
<button type="submit" name="toggle_star" class="star-button">★ <?php echo $project['stars']; ?></button>
</form>
<span>👁️ <?php echo $project['views']; ?> 回閲覧</span>
</div>
</div>
</div>
<div class="project-content">
<div class="project-iframe-container">
<iframe
src="https://soiz1-soiz1-s4s-editor.hf.space/fullscreen.html?project_url=https://drive-proxy-s4s.vercel.app/%3Ffile_id%3D<?php echo urlencode($project['drive_id']); ?>"
class="project-iframe"
allowfullscreen>
</iframe>
</div>
<div class="project-description-container">
<div class="project-description" id="project-description"></div>
</div>
</div>
<?php if ($can_comment): ?>
<div class="comments-section">
<h2 class="comments-title">コメント</h2>
<?php if ($comment_error): ?>
<div class="error-message"><?php echo htmlspecialchars($comment_error); ?></div>
<?php endif; ?>
<?php if ($comment_success): ?>
<div class="success-message"><?php echo htmlspecialchars($comment_success); ?></div>
<?php endif; ?>
<form method="POST" class="comment-form">
<textarea name="comment_content" class="comment-textarea" placeholder="コメントを入力 (マークダウン対応)"></textarea>
<button type="button" class="preview-toggle" onclick="togglePreview(this)">プレビューを表示</button>
<div class="comment-preview"></div>
<button type="submit" class="comment-submit">コメントを投稿</button>
</form>
<div class="comment-list">
<?php foreach ($comments as $index => $comment):
$is_owner = ($_SESSION['user_id'] === $project['author_id']);
$is_comment_owner = ($comment['user_id'] === $_SESSION['user_id']);
$can_edit = $is_owner || $is_comment_owner;
$user_icon = $comment['icon_url'] ?? '';
?>
<div class="comment-item" id="comment-<?php echo $index; ?>">
<div class="comment-header">
<?php if ($user_icon): ?>
<img src="<?php echo htmlspecialchars($user_icon); ?>" class="comment-user-icon" alt="ユーザーアイコン">
<?php endif; ?>
<span class="comment-user-name"><?php echo htmlspecialchars($comment['name']); ?></span>
<span class="comment-time">
<?php echo date('Y/m/d H:i', $comment['timestamp'] ?? time()); ?>
<?php if (!empty($comment['edited'])): ?>
(編集済み)
<?php endif; ?>
</span>
</div>
<div class="comment-content" id="comment-content-<?php echo $index; ?>" style="display:none;">
<?php echo htmlspecialchars($comment['content'], ENT_QUOTES, 'UTF-8'); ?>
</div>
<div class="comment-render" id="comment-render-<?php echo $index; ?>"></div>
<?php if ($can_edit): ?>
<div class="comment-actions">
<button type="button" class="comment-action-button" onclick="showEditForm(<?php echo $index; ?>)">編集</button>
<form method="POST" style="display:inline;">
<input type="hidden" name="comment_index" value="<?php echo $index; ?>">
<button type="submit" name="comment_action" value="delete" class="comment-action-button" onclick="return confirm('本当に削除しますか?');">削除</button>
</form>
</div>
<form method="POST" class="comment-edit-form" id="edit-form-<?php echo $index; ?>">
<input type="hidden" name="comment_index" value="<?php echo $index; ?>">
<textarea name="edited_content" class="comment-textarea"><?php echo htmlspecialchars($comment['content']); ?></textarea>
<button type="button" class="preview-toggle" onclick="toggleEditPreview(<?php echo $index; ?>)">プレビューを表示</button>
<div class="comment-preview" id="edit-preview-<?php echo $index; ?>"></div>
<button type="submit" name="comment_action" value="edit" class="comment-submit">更新</button>
<button type="button" class="comment-action-button" onclick="hideEditForm(<?php echo $index; ?>)">キャンセル</button>
</form>
<?php endif; ?>
</div>
<?php endforeach; ?>
<?php if (empty($comments)): ?>
<p>まだコメントはありません</p>
<?php endif; ?>
</div>
</div>
<?php endif; ?>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
// プロジェクト説明をマークダウンでレンダリング
const description = document.getElementById('project-description');
if (description) {
const markdown = `<?php echo addslashes($project['description']); ?>`;
description.innerHTML = marked.parse(markdown);
}
// コメント内容をマークダウンでレンダリング
document.querySelectorAll('.comment-content').forEach((element, i) => {
const raw = element.textContent;
const renderTarget = document.getElementById(`comment-render-${i}`);
renderTarget.innerHTML = marked.parse(raw);
});
// サムネイル読み込み
const accessToken = '<?php echo $_SESSION['access_token'] ?? ''; ?>';
const thumbnailContainer = document.getElementById('project-thumbnail');
if (thumbnailContainer) {
const driveId = thumbnailContainer.dataset.driveId;
const loadingElement = thumbnailContainer.querySelector('.loading-thumbnail');
if (!driveId) {
loadingElement.textContent = 'サムネイルなし';
return;
}
const thumbnailName = `Scratch-Thumbnail-${driveId}.png`;
// サムネイルを検索
fetch(`https://www.googleapis.com/drive/v3/files?q=name='${encodeURIComponent(thumbnailName)}'`, {
headers: {
'Authorization': `Bearer ${accessToken}`
}
})
.then(response => {
if (!response.ok) {
throw new Error('サムネイル検索に失敗しました');
}
return response.json();
})
.then(data => {
if (data.files && data.files.length > 0) {
const thumbnailId = data.files[0].id;
const img = document.createElement('img');
img.src = `https://drive.google.com/thumbnail?id=${thumbnailId}&sz=w300`;
img.alt = 'プロジェクトサムネイル';
img.onload = () => {
loadingElement.remove();
thumbnailContainer.appendChild(img);
};
img.onerror = () => {
loadingElement.textContent = 'サムネイル読み込みエラー';
};
} else {
loadingElement.textContent = 'サムネイルなし';
}
})
.catch(error => {
console.error('サムネイル取得エラー:', error);
loadingElement.textContent = '読み込みエラー';
if (error.message.includes('401')) {
// トークンが無効な場合、ログインページにリダイレクト
window.location.href = 'login.php';
}
});
}
});
// コメントプレビュー機能
function togglePreview(button) {
const form = button.closest('.comment-form');
const textarea = form.querySelector('.comment-textarea');
const preview = form.querySelector('.comment-preview');
if (preview.style.display === 'block') {
preview.style.display = 'none';
button.textContent = 'プレビューを表示';
} else {
preview.innerHTML = marked.parse(textarea.value);
preview.style.display = 'block';
button.textContent = 'プレビューを非表示';
}
}
// コメント編集フォーム表示
function showEditForm(index) {
document.getElementById(`edit-form-${index}`).style.display = 'block';
document.getElementById(`comment-content-${index}`).style.display = 'none';
}
// コメント編集フォーム非表示
function hideEditForm(index) {
document.getElementById(`edit-form-${index}`).style.display = 'none';
document.getElementById(`comment-content-${index}`).style.display = 'block';
}
// 編集プレビュー機能
function toggleEditPreview(index) {
const form = document.getElementById(`edit-form-${index}`);
const textarea = form.querySelector('.comment-textarea');
const preview = document.getElementById(`edit-preview-${index}`);
const button = form.querySelector('.preview-toggle');
if (preview.style.display === 'block') {
preview.style.display = 'none';
button.textContent = 'プレビューを表示';
} else {
preview.innerHTML = marked.parse(textarea.value);
preview.style.display = 'block';
button.textContent = 'プレビューを非表示';
}
}
</script>
</body>
</html>