リブログ記事”たくさん撮った写真があるのでホームページで公開するアプリをAIに作らせました”
写真がたくさんあっても大丈夫。コードはほとんどAIが作ってくれますあとは調整のみAIが細かいコードを作ってくれるので、やりたいことをちゃんと伝えるだけだが、言葉の意味が食い違うことがあるし、細かいことは伝えないで、自分で生成されたコードを修正したほうが早いことがあります。とにかくAIと一緒にプログラミングに慣れることが必要ですね。Raw & Cinematic - Portfolioclinic.mond.jpindex.html<!DOCTYPE html><html lang="ja"><head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Raw & Cinematic - Portfolio</title> <link rel="stylesheet" href="style.css"></head><body> <aside class="sidebar"> <h1 class="logo">PHOTOGRAPHER</h1> <p class="sub-logo">ARCHIVE / 3000</p> <nav class="tag-container"> <p class="tag-title">CATEGORIES</p> <ul id="tag-list"> <li class="tag-item active" data-tag="">ALL IMAGES</li> </ul> </nav> </aside> <main class="gallery-wrapper"> <div class="gallery-grid" id="gallery-grid"> </div> <div id="load-more-trigger" class="loading-trigger">LOADING NEXT SHOTS...</div> </main> <div id="lightbox" class="lightbox"> <span class="lightbox-close">×</span> <div class="lightbox-content"> <img id="lightbox-img" src="" alt=""> <div class="lightbox-meta"> <h2 id="meta-title"></h2> <p id="meta-subject"></p> <p id="meta-comment"></p> <div id="meta-tags"></div> </div> </div> </div> <script src="main.js"></script></body></html>api.js<?phpheader('Content-Type: application/json; charset=utf-8');try { $db = new PDO('sqlite:gallery.db'); $db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $action = $_GET['action'] ?? ''; // 【機能1】タグ一覧の取得 if ($action === 'get_tags') { // タグがカンマ区切りの場合を考慮(簡易版。実運用では一意のタグを抽出) // ここでは単純に登録されているタグのユニーク一覧を取得します $stmt = $db->query("SELECT DISTINCT tags FROM photos WHERE tags IS NOT NULL AND tags != ''"); $tags = $stmt->fetchAll(PDO::FETCH_COLUMN); // カンマ区切りのタグを分解して重複排除 $all_tags = []; foreach ($tags as $t) { $split = array_map('trim', explode(';', $t)); $all_tags = array_merge($all_tags, $split); } echo json_encode(array_values(array_unique($all_tags))); exit; } // 【機能2】写真一覧の取得(ページネーション付き) $tag = $_GET['tag'] ?? ''; $page = isset($_GET['page']) ? (int)$_GET['page'] : 1; $limit = 24; // 1回に変える枚数(レスポンス向上) $offset = ($page - 1) * $limit; if (!empty($tag)) { // 特定のタグを含む写真を前方・部分一致で高速検索 $sql = "SELECT filename, title, subject, tags, comment FROM photos WHERE tags LIKE :tags LIMIT :limit OFFSET :offset"; $stmt = $db->prepare($sql); $stmt->bindValue(':tags', '%' . $tag . '%', PDO::PARAM_STR); } else { // タグ指定なし(全件表示) $sql = "SELECT filename, title, subject, tags, comment FROM photos LIMIT :limit OFFSET :offset"; $stmt = $db->prepare($sql); } $stmt->bindValue(':limit', $limit, PDO::PARAM_INT); $stmt->bindValue(':offset', $offset, PDO::PARAM_INT); $stmt->execute(); $photos = $stmt->fetchAll(PDO::FETCH_ASSOC); echo json_encode($photos);} catch (PDOException $e) { echo json_encode(['error' => $e->getMessage()]);}main.jslet currentPage = 1;let currentTag = "";let isLoading = false;let hasMore = true;const grid = document.getElementById('gallery-grid');const tagList = document.getElementById('tag-list');const trigger = document.getElementById('load-more-trigger');// --- 1. 写真データをAPIから取得して描画 ---async function fetchPhotos(replace = false) { if (isLoading || (!hasMore && !replace)) return; isLoading = true; if (replace) { currentPage = 1; grid.innerHTML = ""; hasMore = true;//console.log("replace"); } try {// const response = await fetch(`api.php?tag=${encodeURIComponent(currentTag)}&page=${currentPage}`); const response = await fetch('api.php?tag='+encodeURIComponent(currentTag)+'&page='+currentPage); const photos = await response.json(); if (photos.length < 24) { hasMore = false; trigger.style.display = 'none'; } else { trigger.style.display = 'block'; } photos.forEach(photo => { const item = document.createElement('div'); item.className = 'grid-item'; item.innerHTML = `<img src="picture/${photo.filename}" alt="${photo.title || ''}" loading="lazy">`; // クリック時にライトボックスを開く item.addEventListener('click', () => openLightbox(photo)); grid.appendChild(item); }); currentPage++; } catch (e) { console.error("Failed to fetch photos:", e); } finally { isLoading = false; }}// --- 2. タグ一覧をAPIから取得 ---async function fetchTags() { try { const response = await fetch('api.php?action=get_tags'); const tags = await response.json(); tags.forEach(tag => { const li = document.createElement('li'); li.className = 'tag-item'; li.textContent = tag.toUpperCase(); li.setAttribute('data-tag', tag); tagList.appendChild(li); }); } catch (e) { console.error("Failed to fetch tags:", e); }}// --- 3. タグ選択イベント ---tagList.addEventListener('click', (e) => { if (!e.target.classList.contains('tag-item')) return; document.querySelectorAll('.tag-item').forEach(el => el.classList.remove('active')); e.target.classList.add('active'); currentTag = e.target.getAttribute('data-tag'); fetchPhotos(true); // グリッドをリセットして再読み込み});// --- 4. 無限スクロールの監視 (最下部検知) ---const observer = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasMore && !isLoading) { fetchPhotos(); }}, { rootMargin: '400px' }); // 画面外400px手前で先読みobserver.observe(trigger);// --- 5. ライトボックス制御 ---const lightbox = document.getElementById('lightbox');function openLightbox(photo) { document.getElementById('lightbox-img').src = `picture/${photo.filename}`; document.getElementById('meta-title').textContent = photo.title || ''; document.getElementById('meta-subject').textContent = photo.subject || ''; document.getElementById('meta-comment').textContent = photo.comment || ''; document.getElementById('meta-tags').textContent = photo.tag ? `TAGS: ${photo.tag}` : ''; lightbox.classList.add('open');}document.querySelector('.lightbox-close').addEventListener('click', () => { lightbox.classList.remove('open');});lightbox.addEventListener('click', (e) => { if (e.target === lightbox) lightbox.classList.remove('open');});// --- 初期起動 ---fetchTags();fetchPhotos();style.css/* --- リセット & 基本設定 --- */* { margin: 0; padding: 0; box-sizing: border-box; }body { background-color: #0a0a0a; color: #fff; font-family: 'Helvetica Neue', Arial, sans-serif; display: flex; overflow: hidden;}/* --- サイドバー(固定) --- */.sidebar { width: 300px; height: 100vh; padding: 3rem 2rem; display: flex; flex-direction: column; border-right: 1px solid #222; background: #0a0a0a; z-index: 10;}.logo { font-size: 1.5rem; font-weight: 900; letter-spacing: 0.2em; flex-shrink: 0; }.sub-logo { font-size: 0.8rem; color: #666; margin-bottom: 3rem; flex-shrink: 0; }/* ここがポイント! タグのコンテナ自体に高さを柔軟に持たせ、溢れたらスクロールさせます。*/.tag-container { flex: 1; /* 残りの縦幅をすべてこのエリアに割り当てる */ display: flex; flex-direction: column; overflow: hidden; /* 外枠ははみ出さないように固定 */}.tag-title { font-size: 0.75rem; color: #444; letter-spacing: 0.1em; margin-bottom: 1rem; flex-shrink: 0; }/* 実際のタグリスト部分 ここをスクロール可能にし、さらにスタイリッシュに見せる工夫をします。*/.tag-container ul { list-style: none; overflow-y: auto; /* 縦方向のスクロールを有効にする */ padding-right: 10px; /* スクロールバーのカスタマイズ(個性的でミニマルなデザイン) Firefox用 */ scrollbar-width: thin; scrollbar-color: #333 #0a0a0a;}/* Chrome, Safari, Edge用のスクロールバーデザイン */.tag-container ul::-webkit-scrollbar { width: 4px; /* 極細にして目立たせない */}.tag-container ul::-webkit-scrollbar-track { background: #0a0a0a;}.tag-container ul::-webkit-scrollbar-thumb { background: #222; /* 普段は薄く */ border-radius: 2px;}.tag-container ul::-webkit-scrollbar-thumb:hover { background: #444; /* ホバーしたときだけ少し明るく */}.tag-item { font-size: 0.9rem; color: #888; padding: 0.5rem 0; cursor: pointer; transition: all 0.3s;}.tag-item:hover, .tag-item.active { color: #fff; padding-left: 5px; }.logo { font-size: 1.5rem; font-weight: 900; letter-spacing: 0.2em; }.sub-logo { font-size: 0.8rem; color: #666; margin-bottom: 3rem; }.tag-title { font-size: 0.75rem; color: #444; letter-spacing: 0.1em; margin-bottom: 1rem; }.tag-container ul { list-style: none; }.tag-item { font-size: 0.9rem; color: #888; padding: 0.5rem 0; cursor: pointer; transition: all 0.3s;}.tag-item:hover, .tag-item.active { color: #fff; padding-left: 5px; }/* --- ギャラリー(不規則なグリッドで個性を出す) --- */.gallery-wrapper { flex: 1; height: 100vh; overflow-y: auto; padding: 2rem; scroll-behavior: smooth;}.gallery-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); grid-auto-rows: 250px; grid-auto-flow: dense; /* 隙間を自動で埋める */ gap: 15px;}.grid-item { position: relative; overflow: hidden; cursor: pointer; background: #111;}/* 個性的なレイアウトを作るためのトリッキーなCSS(縦長・横長をランダムミックス) */.grid-item:nth-child(5n+1) { grid-column: span 2; grid-row: span 2; } /* 特大 */.grid-item:nth-child(7n+3) { grid-row: span 2; } /* 縦長 */.grid-item:nth-child(8n+5) { grid-column: span 2; } /* 横長 */.grid-item img { width: 100%; height: 100%; object-fit: cover; filter: grayscale(30%) brightness(0.9); transition: all 0.6s cubic-bezier(0.25, 1, 0.5, 1);}.grid-item:hover img { filter: grayscale(0%) brightness(1); transform: scale(1.05);}/* --- ライトボックス(映画のスクリーンのように表示) --- */.lightbox { position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: rgba(0, 0, 0, 0.95); z-index: 100; display: flex; justify-content: center; align-items: center; opacity: 0; pointer-events: none; transition: opacity 0.4s ease;}.lightbox.open { opacity: 1; pointer-events: auto; }.lightbox-close { position: absolute; top: 2rem; right: 2rem; font-size: 3rem; cursor: pointer; color: #666;}.lightbox-content { display: flex; max-width: 90%; max-height: 85vh; gap: 2rem;}#lightbox-img { max-width: 70%; max-height: 85vh; object-fit: contain; box-shadow: 0 0 50px rgba(0,0,0,0.8);}.lightbox-meta { flex: 1; color: #ccc; display: flex; flex-direction: column; justify-content: center;}#meta-title { font-size: 1.8rem; color: #fff; margin-bottom: 0.5rem; font-weight: 300; }#meta-subject { font-size: 1rem; color: #888; margin-bottom: 1.5rem; }#meta-comment { font-size: 0.95rem; line-height: 1.6; margin-bottom: 1.5rem; }.loading-trigger { text-align: center; padding: 3rem; color: #444; font-size: 0.8rem; letter-spacing: 0.2em; }/* レスポンシブ */@media (max-width: 900px) { body { flex-direction: column; } .sidebar { width: 100%; height: auto; border-right: none; border-bottom: 1px solid #222; } .gallery-wrapper { height: auto; } .lightbox-content { flex-direction: column; } #lightbox-img { max-width: 100%; }}