<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>仮想OS Pro</title>
<style>
body {
margin: 0;
background: #2c3e50;
font-family: 'Segoe UI', sans-serif;
overflow: hidden;
}
#desktop {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 40px;
background: linear-gradient(#2980b9, #34495e);
}
.icon {
width: 70px;
text-align: center;
margin: 20px;
cursor: pointer;
color: white;
}
.window {
position: absolute;
width: 300px;
height: 200px;
background: white;
border: 2px solid #555;
display: none;
box-shadow: 4px 4px 10px rgba(0,0,0,0.5);
}
.window-header {
background: #3498db;
padding: 5px;
cursor: move;
color: white;
}
.window-body {
padding: 10px;
}
#taskbar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 40px;
background: #2c3e50;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 10px;
color: white;
}
</style>
</head>
<body onload="playStartupSound(); updateClock(); setInterval(updateClock, 1000);">
<div id="desktop">
<div class="icon" onclick="openWindow('memo')">
<br>メモ帳</div>
<div class="icon" onclick="openWindow('calc')">
<br>電卓</div>
</div>
<div id="taskbar">
<div>仮想OS Pro</div>
<div id="clock"></div>
</div>
<!-- メモ帳 -->
<div class="window" id="memo">
<div class="window-header" onmousedown="dragWindow(event, this.parentElement)">メモ帳</div>
<div class="window-body">
<textarea style="width: 100%; height: 100px;">メモを入力してください</textarea>
</div>
</div>
<!-- 電卓 -->
<div class="window" id="calc">
<div class="window-header" onmousedown="dragWindow(event, this.parentElement)">電卓</div>
<div class="window-body">
<input type="text" id="calcDisplay" style="width:100%; font-size: 1.2em;" />
<button onclick="calculate()">計算</button>
</div>
</div>
<!-- 起動音 -->
<audio id="bootSound" src="https://upload.wikimedia.org/wikipedia/commons/2/2f/Windows_95_startup.ogg" preload="auto"></audio>
<script>
function openWindow(id) {
document.getElementById(id).style.display = 'block';
}
function dragWindow(e, el) {
e.preventDefault();
let offsetX = e.clientX - el.offsetLeft;
let offsetY = e.clientY - el.offsetTop;
function move(e) {
el.style.left = (e.clientX - offsetX) + 'px';
el.style.top = (e.clientY - offsetY) + 'px';
}
function stop() {
document.removeEventListener('mousemove', move);
document.removeEventListener('mouseup', stop);
}
document.addEventListener('mousemove', move);
document.addEventListener('mouseup', stop);
}
function calculate() {
let result;
try {
result = eval(document.getElementById('calcDisplay').value);
} catch {
result = "エラー";
}
document.getElementById('calcDisplay').value = result;
}
function playStartupSound() {
document.getElementById("bootSound").play();
}
function updateClock() {
const now = new Date();
const time = now.toLocaleTimeString();
document.getElementById("clock").textContent = time;
}
</script>
</body>
</html>
カテゴリー: HTML
C#入門サイト
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="description" content="C# オールインワン入門サイト">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>オールインワン C# 入門サイト</title>
<!-- ★ スタイル(CSS)をまとめて定義 ★ -->
<style>
/* ページ全体 */
body {
margin: 0;
padding: 0;
font-family: sans-serif;
line-height: 1.6;
background: #f9f9f9;
}
/* ヘッダー */
header {
background: #f0f0f0;
padding: 10px;
}
header h1 {
margin: 0;
font-size: 1.5em;
}
/* ナビゲーション */
nav ul {
list-style: none;
padding: 0;
display: flex;
gap: 10px;
margin-top: 5px;
}
nav a {
text-decoration: none;
color: #333;
font-weight: bold;
}
nav a:hover {
text-decoration: underline;
}
/* メインコンテンツ */
main {
max-width: 900px;
margin: 20px auto;
padding: 20px;
background: #fff;
box-shadow: 0 0 4px rgba(0,0,0,0.1);
}
main h2 {
margin-top: 0;
border-left: 6px solid #333;
padding-left: 8px;
margin-bottom: 15px;
}
main h3 {
margin-bottom: 5px;
}
/* コードブロック */
pre {
background: #fafafa;
padding: 10px;
overflow-x: auto;
}
code {
font-family: Consolas, monospace;
}
/* フォームなど(クイズ用) */
form section {
margin-bottom: 20px;
}
#result {
margin-top: 20px;
background: #eef;
padding: 10px;
border: 1px solid #ccf;
display: inline-block;
}
/* フッター */
footer {
text-align: center;
padding: 10px;
background: #f0f0f0;
margin-top: 20px;
}
/* セクション間の余白調整 */
section {
margin-bottom: 40px;
}
</style>
</head>
<body>
<!-- ★ ヘッダーとナビゲーション ★ -->
<header>
<h1>オールインワン C# 入門サイト</h1>
<nav>
<ul>
<li><a href="#top">トップ</a></li>
<li><a href="#environment">環境構築 & .NET</a></li>
<li><a href="#basics">C#の基本</a></li>
<li><a href="#syntax">基本文法</a></li>
<li><a href="#control-flow">条件分岐・ループ</a></li>
<li><a href="#oop">オブジェクト指向</a></li>
<li><a href="#advanced">応用トピック</a></li>
<li><a href="#debugging">デバッグ & テスト</a></li>
<li><a href="#quiz">クイズ</a></li>
<li><a href="#example">サンプル</a></li>
</ul>
</nav>
</header>
<!-- ★ メインコンテンツ ★ -->
<main>
<!-- トップ (id="top") -->
<section id="top">
<h2>このサイトについて</h2>
<p>
ここでは、C# を初めて学ぶ方や、基礎を復習したい方向けに、
C#と.NETの概要から開発環境構築、文法、オブジェクト指向、さらにデバッグやテスト、
一歩進んだ応用トピック(Generics, LINQ, 非同期など)まで幅広くカバーしています。
</p>
<p>
ページ上部のナビゲーションから各セクションへ移動できます。
一通り学習した後は、クイズに挑戦したり、サンプルプログラムを動かしてみましょう。
</p>
</section>
<!-- 環境構築 (id="environment") -->
<section id="environment">
<h2>環境構築 & .NET</h2>
<article>
<h3>.NETエコシステムの概要</h3>
<p>
C# は .NET 上で動作する言語です。
近年はクロスプラットフォーム対応の「.NET (Core)」が主流であり、
Windows だけでなく Mac や Linux でも利用可能です。
</p>
</article>
<article>
<h3>Windows での開発</h3>
<p>
<strong>Visual Studio</strong> (Community 版) または <strong>VS Code</strong> が一般的です。
<code>dotnet</code> CLI を使ってプロジェクト作成・ビルド・実行が可能です。
</p>
<pre><code>
// 例: コンソールアプリプロジェクトの作成
dotnet new console -o MyApp
cd MyApp
dotnet run
</code></pre>
</article>
<article>
<h3>Mac / Linux での開発</h3>
<p>
公式サイトから .NET SDK をインストールすることで、同様に C# を利用できます。
VS Code + C#拡張機能があればブレークポイントによるデバッグも可能です。
</p>
</article>
</section>
<!-- C#の基本 (id="basics") -->
<section id="basics">
<h2>C#の基本</h2>
<article>
<h3>C#とは?</h3>
<p>
C# (シーシャープ) は、マイクロソフトが開発したオブジェクト指向言語です。
Javaに似た構文を持ち、GenericsやLINQ、非同期などモダンな機能を数多く備えています。
</p>
</article>
<article>
<h3>Hello World</h3>
<pre><code class="language-csharp">
using System;
namespace HelloWorldApp
{
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World!");
}
}
}
</code></pre>
<p>このように、<code>Main</code> メソッドがエントリポイントとして呼び出されます。</p>
</article>
</section>
<!-- 基本文法 (id="syntax") -->
<section id="syntax">
<h2>基本文法</h2>
<article>
<h3>変数とデータ型</h3>
<pre><code class="language-csharp">
int number = 10;
double pi = 3.14;
bool isActive = true;
string message = "Hello C#";
</code></pre>
<p>
C# は強い型付け言語であり、<code>var</code> キーワードで型推論も可能です。
</p>
</article>
<article>
<h3>演算子</h3>
<p>
算術演算子、比較演算子、論理演算子などを使用できます。
</p>
<pre><code class="language-csharp">
int x = 5;
int y = 3;
Console.WriteLine(x + y); // 8
Console.WriteLine(x > y); // true
</code></pre>
</article>
</section>
<!-- 条件分岐・ループ (id="control-flow") -->
<section id="control-flow">
<h2>条件分岐・ループ</h2>
<article>
<h3>if / else / else if</h3>
<pre><code class="language-csharp">
int score = 85;
if(score >= 80) {
Console.WriteLine("Excellent!");
} else if(score >= 60) {
Console.WriteLine("Good!");
} else {
Console.WriteLine("Keep trying!");
}
</code></pre>
</article>
<article>
<h3>switch</h3>
<pre><code class="language-csharp">
int dayOfWeek = 2;
switch(dayOfWeek)
{
case 0:
Console.WriteLine("日曜日");
break;
case 1:
Console.WriteLine("月曜日");
break;
case 2:
Console.WriteLine("火曜日");
break;
default:
Console.WriteLine("不明な曜日");
break;
}
</code></pre>
</article>
<article>
<h3>for / while / do-while</h3>
<pre><code class="language-csharp">
// forループ
for(int i = 0; i < 5; i++) {
Console.WriteLine(i);
}
// whileループ
int j = 0;
while(j < 5) {
Console.WriteLine(j);
j++;
}
// do-whileループ
int k = 0;
do {
Console.WriteLine(k);
k++;
} while(k < 5);
</code></pre>
</article>
</section>
<!-- オブジェクト指向 (id="oop") -->
<section id="oop">
<h2>オブジェクト指向</h2>
<article>
<h3>クラスとオブジェクト</h3>
<pre><code class="language-csharp">
class Person
{
public string Name;
public int Age;
public void Greet()
{
Console.WriteLine($"こんにちは、{Name}です。");
}
}
class Program
{
static void Main()
{
Person p = new Person();
p.Name = "Taro";
p.Age = 20;
p.Greet();
}
}
</code></pre>
</article>
<article>
<h3>継承とポリモーフィズム</h3>
<pre><code class="language-csharp">
class Animal
{
public string Name { get; set; }
public virtual void Speak()
{
Console.WriteLine("何かを話す");
}
}
class Dog : Animal
{
public override void Speak()
{
Console.WriteLine("ワンワン");
}
}
</code></pre>
</article>
<article>
<h3>インターフェイス</h3>
<pre><code class="language-csharp">
interface IFlyable
{
void Fly();
}
class Bird : IFlyable
{
public void Fly()
{
Console.WriteLine("羽ばたいて飛ぶ");
}
}
</code></pre>
</article>
</section>
<!-- 応用トピック (id="advanced") -->
<section id="advanced">
<h2>応用トピック</h2>
<article>
<h3>Generics (ジェネリクス)</h3>
<pre><code class="language-csharp">
List<int> numbers = new List<int>();
numbers.Add(10);
numbers.Add(20);
class Box<T>
{
public T Value { get; set; }
public Box(T value)
{
Value = value;
}
}
</code></pre>
</article>
<article>
<h3>デリゲート・イベント</h3>
<pre><code class="language-csharp">
public delegate void MyDelegate(string msg);
public class Publisher
{
public event MyDelegate OnPublish;
public void Publish(string msg)
{
OnPublish?.Invoke(msg);
}
}
</code></pre>
</article>
<article>
<h3>LINQ</h3>
<pre><code class="language-csharp">
int[] data = {1,2,3,4,5,6};
var evens = from x in data
where x % 2 == 0
select x;
</code></pre>
</article>
<article>
<h3>非同期 (async/await)</h3>
<pre><code class="language-csharp">
static async Task Main()
{
Console.WriteLine("開始");
await Task.Delay(1000);
Console.WriteLine("終了");
}
</code></pre>
</article>
</section>
<!-- デバッグ & テスト (id="debugging") -->
<section id="debugging">
<h2>デバッグ & テスト</h2>
<article>
<h3>Visual Studioでのデバッグ</h3>
<ol>
<li>行番号の左をクリックしてブレークポイントを設定</li>
<li>「デバッグ実行」ボタンで開始</li>
<li>停止したらローカル変数やステップ実行を確認</li>
</ol>
</article>
<article>
<h3>例外処理</h3>
<pre><code class="language-csharp">
try
{
int num = int.Parse("abc");
}
catch(FormatException fe)
{
Console.WriteLine("フォーマットエラー: " + fe.Message);
}
finally
{
Console.WriteLine("終了処理");
}
</code></pre>
</article>
<article>
<h3>ユニットテスト</h3>
<pre><code class="language-csharp">
using Xunit;
public class CalcTests
{
[Fact]
public void AddTest()
{
int result = Calc.Add(2, 3);
Assert.Equal(5, result);
}
}
public static class Calc
{
public static int Add(int x, int y) => x + y;
}
</code></pre>
<p>
xUnit、NUnit、MSTest などが有名です。<code>dotnet test</code> でテスト実行できます。
</p>
</article>
</section>
<!-- クイズ (id="quiz") -->
<section id="quiz">
<h2>C#クイズ</h2>
<p>ラジオボタンで答えを選んで「採点する」ボタンを押してください。</p>
<form id="quizForm">
<article>
<h3>Q1. C# コンソールアプリのエントリポイントは?</h3>
<label>
<input type="radio" name="q1" value="A">
public static void start()
</label><br>
<label>
<input type="radio" name="q1" value="B">
private void main()
</label><br>
<label>
<input type="radio" name="q1" value="C">
static void Main(string[] args)
</label><br>
<label>
<input type="radio" name="q1" value="D">
run()
</label>
</article>
<article>
<h3>Q2. 次のうち整数型ではないのは?</h3>
<label>
<input type="radio" name="q2" value="A">
int
</label><br>
<label>
<input type="radio" name="q2" value="B">
double
</label><br>
<label>
<input type="radio" name="q2" value="C">
long
</label><br>
<label>
<input type="radio" name="q2" value="D">
short
</label>
</article>
<article>
<h3>Q3. C# の例外処理に使われるキーワードの組み合わせは?</h3>
<label>
<input type="radio" name="q3" value="A">
try / catch / else
</label><br>
<label>
<input type="radio" name="q3" value="B">
try / check / throw
</label><br>
<label>
<input type="radio" name="q3" value="C">
try / catch / finally
</label><br>
<label>
<input type="radio" name="q3" value="D">
try / except / ensure
</label>
</article>
<button type="button" onclick="gradeQuiz()">採点する</button>
</form>
<!-- 採点結果を表示する領域 -->
<div id="result"></div>
</section>
<!-- サンプル (id="example") -->
<section id="example">
<h2>サンプルプログラム</h2>
<article>
<h3>1. ユーザー入力を受け取る</h3>
<pre><code class="language-csharp">
using System;
namespace SampleApp
{
class Program
{
static void Main()
{
Console.WriteLine("名前を入力してください:");
string name = Console.ReadLine();
Console.WriteLine($"こんにちは、{name}さん!");
}
}
}
</code></pre>
</article>
<article>
<h3>2. 例外処理</h3>
<pre><code class="language-csharp">
using System;
namespace SampleApp
{
class Program
{
static void Main()
{
Console.WriteLine("整数を入力してください:");
try
{
int number = int.Parse(Console.ReadLine());
Console.WriteLine("二乗:" + (number * number));
}
catch(Exception e)
{
Console.WriteLine("エラー:" + e.Message);
}
}
}
}
</code></pre>
</article>
<article>
<h3>3. LINQ を使ったフィルタリング</h3>
<pre><code class="language-csharp">
using System;
using System.Linq;
using System.Collections.Generic;
namespace SampleApp
{
class Program
{
static void Main()
{
List<int> numbers = new List<int> { 1, 4, 7, 2, 9, 12 };
var evenNumbers = numbers.Where(x => x % 2 == 0);
Console.WriteLine("偶数のみ抽出:");
foreach(var n in evenNumbers)
{
Console.WriteLine(n);
}
}
}
}
</code></pre>
</article>
<article>
<h3>4. 非同期処理</h3>
<pre><code class="language-csharp">
using System;
using System.Threading.Tasks;
namespace SampleApp
{
class Program
{
static async Task Main()
{
Console.WriteLine("開始");
await Task.Delay(2000);
Console.WriteLine("2秒後に終了");
}
}
}
</code></pre>
</article>
</section>
</main>
<!-- フッター -->
<footer>
<p>© 2025 C# Tutorial Site</p>
</footer>
<!-- ★ クイズ判定用JavaScriptをまとめて定義 ★ -->
<script>
// 正解のマッピング(q1,q2,q3,...)
const correctAnswers = {
q1: "C", // static void Main(string[] args)
q2: "B", // doubleは整数型ではない
q3: "C" // try / catch / finally
};
function gradeQuiz() {
const form = document.getElementById("quizForm");
let score = 0;
let total = Object.keys(correctAnswers).length;
// 各問題について、ラジオボタンの選択値をチェック
for(const [question, answer] of Object.entries(correctAnswers)) {
const userAnswer = form.elements[question].value;
if(userAnswer === answer) {
score++;
}
}
// 結果表示
const resultDiv = document.getElementById("result");
resultDiv.innerHTML = `
<h3>採点結果: ${score} / ${total} 正解</h3>
<p>${getComment(score, total)}</p>
`;
}
// 点数に応じたコメントを返す
function getComment(score, total) {
if(score === total) {
return "パーフェクト!素晴らしいです!";
} else if(score >= total - 1) {
return "惜しい!あともう少し!";
} else {
return "まだまだ勉強が必要です。がんばりましょう!";
}
}
</script>
</body>
</html>
Gamestore
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>GameStore Ultra - Upload & Persist Data</title>
<meta name="description" content="高度なインタラクションと洗練されたデザイン、アップロードデータの保持機能を実現したゲームストアサイト。">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
/*────────────────────────────
グローバルリセット&変数設定
────────────────────────────*/
:root {
--primary: #ff6600;
--accent: #e55a00;
--secondary: #222;
--light: #fff;
--dark: #333;
--text: #333;
--shadow: 0 2px 8px rgba(0,0,0,0.15);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
html { scroll-behavior: smooth; }
body { font-family: 'Helvetica Neue', Arial, sans-serif; background: #f8f8f8; color: var(--text); }
a { text-decoration: none; color: inherit; }
img { display: block; max-width: 100%; height: auto; }
/*────────────────────────────
ヘッダー&ナビゲーション
────────────────────────────*/
header {
background: var(--secondary);
color: var(--light);
padding: 10px 20px;
position: sticky;
top: 0;
z-index: 1000;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow);
}
.logo { display: flex; align-items: center; gap: 10px; }
.logo img { width: 40px; height: 40px; }
.logo h1 { font-size: 24px; }
nav { background: var(--dark); }
.nav-container {
max-width: 1200px;
margin: auto;
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 20px;
}
.nav-menu { list-style: none; display: flex; gap: 20px; }
.nav-menu li { position: relative; }
.nav-menu li a {
color: var(--light);
padding: 8px 10px;
transition: background 0.3s;
}
.nav-menu li a:hover { background: #444; }
/* ドロップダウン */
.dropdown {
position: absolute;
top: 100%;
left: 0;
background: var(--dark);
display: none;
flex-direction: column;
min-width: 150px;
}
.dropdown li a { padding: 8px 10px; }
.nav-menu li:hover .dropdown { display: flex; }
/* アップロードリンク(管理者向け) */
.nav-menu li.upload-link { margin-left: auto; }
/* ハンバーガーメニュー(モバイル) */
.hamburger {
display: none;
flex-direction: column;
gap: 4px;
cursor: pointer;
}
.hamburger span { width: 25px; height: 3px; background: var(--light); }
@media (max-width: 768px) {
.nav-menu { display: none; flex-direction: column; width: 100%; }
.nav-menu.active { display: flex; }
.hamburger { display: flex; }
.nav-container { flex-direction: column; align-items: flex-start; }
}
.search-container { position: relative; }
.search-container input[type="text"] {
padding: 6px 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.search-container button {
position: absolute;
right: 0;
top: 0;
bottom: 0;
background: var(--primary);
border: none;
padding: 6px 10px;
border-radius: 0 4px 4px 0;
color: var(--light);
cursor: pointer;
}
/*────────────────────────────
ヒーロースライダー
────────────────────────────*/
.hero-slider {
position: relative;
overflow: hidden;
height: 600px;
}
.hero-slide {
position: absolute;
top: 0; left: 0;
width: 100%;
height: 100%;
background-size: cover;
background-position: center;
opacity: 0;
transition: opacity 1s ease;
display: flex;
align-items: center;
justify-content: center;
}
.hero-slide.active { opacity: 1; position: relative; }
.hero-content {
background: rgba(0, 0, 0, 0.6);
padding: 20px 30px;
border-radius: 8px;
text-align: center;
color: var(--light);
}
.hero-content h2 { font-size: 48px; margin-bottom: 15px; }
.hero-content p { font-size: 20px; margin-bottom: 20px; }
.hero-content .btn {
background: var(--primary);
padding: 12px 24px;
border-radius: 4px;
transition: background 0.3s;
}
.hero-content .btn:hover { background: var(--accent); }
.hero-controls {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 8px;
}
.hero-control {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--light);
opacity: 0.5;
cursor: pointer;
}
.hero-control.active { opacity: 1; }
/*────────────────────────────
スクロール時フェードインアニメーション
────────────────────────────*/
.fade-in {
opacity: 0;
transform: translateY(20px);
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
}
.fade-in.visible { opacity: 1; transform: none; }
/*────────────────────────────
セクション共通スタイル
────────────────────────────*/
section {
padding: 60px 20px;
max-width: 1200px;
margin: auto;
}
.section-title {
font-size: 32px;
margin-bottom: 20px;
border-left: 5px solid var(--primary);
padding-left: 10px;
}
.section-description {
font-size: 18px;
margin-bottom: 30px;
color: #666;
}
/*────────────────────────────
ゲームカードグリッド
────────────────────────────*/
.game-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
}
.game-card {
background: var(--light);
border-radius: 8px;
box-shadow: var(--shadow);
overflow: hidden;
cursor: pointer;
position: relative;
transition: transform 0.3s;
}
.game-card:hover { transform: translateY(-5px); }
.game-card img {
width: 100%;
height: 180px;
object-fit: cover;
}
.game-card-content { padding: 15px; }
.game-card-content h3 { font-size: 22px; margin-bottom: 10px; }
.game-card-content p { font-size: 16px; color: #666; margin-bottom: 10px; }
.game-card-content .price { font-size: 20px; color: var(--primary); }
.card-overlay {
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.3);
opacity: 0;
transition: opacity 0.3s;
}
.game-card:hover .card-overlay { opacity: 1; }
/*────────────────────────────
カテゴリセクション
────────────────────────────*/
.categories {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 20px;
}
.category-card {
position: relative;
border-radius: 8px;
overflow: hidden;
cursor: pointer;
}
.category-card img {
width: 100%;
height: 150px;
object-fit: cover;
filter: brightness(70%);
transition: filter 0.3s;
}
.category-card:hover img { filter: brightness(100%); }
.category-card span {
position: absolute;
bottom: 10px;
left: 10px;
background: rgba(0,0,0,0.5);
color: var(--light);
padding: 8px 12px;
border-radius: 4px;
font-size: 18px;
}
/*────────────────────────────
ユーザーレビュー・スライダー
────────────────────────────*/
.reviews-slider {
position: relative;
overflow: hidden;
padding: 20px 0;
}
.reviews-track {
display: flex;
gap: 20px;
transition: transform 0.5s ease-in-out;
}
.review-card {
background: var(--light);
border-radius: 8px;
box-shadow: var(--shadow);
padding: 20px;
min-width: 320px;
}
.review-card h4 { font-size: 20px; margin-bottom: 10px; }
.review-card .rating { margin-bottom: 10px; }
.review-card .rating span { color: gold; font-size: 22px; }
.review-card p { font-size: 16px; color: #444; }
/*────────────────────────────
ブログセクション
────────────────────────────*/
.blog-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 20px;
}
.blog-card {
background: var(--light);
border-radius: 8px;
box-shadow: var(--shadow);
overflow: hidden;
}
.blog-card img {
width: 100%;
height: 200px;
object-fit: cover;
}
.blog-card-content { padding: 15px; }
.blog-card-content h3 { font-size: 22px; margin-bottom: 10px; }
.blog-card-content p { font-size: 16px; color: #666; margin-bottom: 10px; }
.blog-card-content .btn {
background: var(--primary);
padding: 8px 16px;
border-radius: 4px;
transition: background 0.3s;
font-size: 16px;
}
.blog-card-content .btn:hover { background: var(--accent); }
/*────────────────────────────
モーダル(商品詳細、ログイン、登録、カート、アップロード)
────────────────────────────*/
.modal-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6);
display: none;
align-items: center;
justify-content: center;
z-index: 2000;
}
.modal-overlay.active { display: flex; }
.modal {
background: var(--light);
max-width: 600px;
width: 90%;
border-radius: 8px;
overflow: hidden;
animation: modalFade 0.3s;
position: relative;
}
@keyframes modalFade {
from { opacity: 0; transform: translateY(-20px); }
to { opacity: 1; transform: translateY(0); }
}
.modal-header, .modal-footer { padding: 15px; background: var(--dark); color: var(--light); }
.modal-body { padding: 20px; }
.modal-close {
position: absolute;
top: 15px;
right: 15px;
font-size: 24px;
cursor: pointer;
color: var(--light);
}
/*────────────────────────────
Back-to-Top ボタン
────────────────────────────*/
#backToTop {
position: fixed;
bottom: 40px;
right: 40px;
background: var(--primary);
color: var(--light);
padding: 12px;
border-radius: 50%;
display: none;
cursor: pointer;
box-shadow: var(--shadow);
transition: background 0.3s;
z-index: 1500;
}
#backToTop:hover { background: var(--accent); }
/*────────────────────────────
フィルターサイドバー(デスクトップ)
────────────────────────────*/
.filter-sidebar {
position: fixed;
top: 100px;
left: 20px;
width: 250px;
background: var(--light);
border: 1px solid #ccc;
border-radius: 8px;
padding: 20px;
box-shadow: var(--shadow);
z-index: 1200;
}
.filter-sidebar h3 { font-size: 20px; margin-bottom: 15px; }
.filter-sidebar label { display: block; margin-bottom: 8px; font-size: 16px; }
.filter-sidebar input, .filter-sidebar select {
width: 100%;
padding: 6px 8px;
margin-bottom: 12px;
border: 1px solid #ccc;
border-radius: 4px;
}
.filter-sidebar button {
width: 100%;
padding: 10px;
background: var(--primary);
border: none;
color: var(--light);
border-radius: 4px;
font-size: 16px;
transition: background 0.3s;
cursor: pointer;
}
.filter-sidebar button:hover { background: var(--accent); }
@media (max-width: 1024px) { .filter-sidebar { display: none; } }
</style>
</head>
<body>
<!--────────────────────────────
ヘッダー
────────────────────────────-->
<header>
<div class="logo">
<img src="images/logo.png" alt="GameStore Logo" loading="lazy">
<h1>GameStore Ultra</h1>
</div>
<div class="hamburger" id="hamburger">
<span></span>
<span></span>
<span></span>
</div>
</header>
<!--────────────────────────────
ナビゲーション
────────────────────────────-->
<nav>
<div class="nav-container">
<ul class="nav-menu" id="navMenu">
<li><a href="#">ホーム</a></li>
<li>
<a href="#">新作</a>
<ul class="dropdown">
<li><a href="#">今週の新作</a></li>
<li><a href="#">今月の新作</a></li>
</ul>
</li>
<li>
<a href="#">人気タイトル</a>
<ul class="dropdown">
<li><a href="#">ランキング</a></li>
<li><a href="#">レビュー</a></li>
</ul>
</li>
<li><a href="#">セール</a></li>
<li><a href="#">ブログ</a></li>
<li class="upload-link"><a href="#" id="uploadLink">アップロード</a></li>
</ul>
<div class="search-container">
<input type="text" placeholder="ゲームを検索..." id="searchInput">
<button id="searchButton">検索</button>
</div>
</div>
</nav>
<!--────────────────────────────
フィルターサイドバー(デスクトップ用)
────────────────────────────-->
<aside class="filter-sidebar" id="filterSidebar">
<h3>フィルター</h3>
<label for="filterGenre">ジャンル:</label>
<select id="filterGenre">
<option value="">すべて</option>
<option value="アクション">アクション</option>
<option value="RPG">RPG</option>
<option value="シューティング">シューティング</option>
<option value="パズル">パズル</option>
</select>
<label for="filterPrice">価格上限:</label>
<input type="number" id="filterPrice" placeholder="例: 5000">
<button id="applyFilter">フィルター適用</button>
</aside>
<!--────────────────────────────
ヒーロースライダー
────────────────────────────-->
<div class="hero-slider" id="heroSlider">
<div class="hero-slide active" style="background-image: url('images/hero1.jpg');">
<div class="hero-content">
<h2>最新作が続々登場!</h2>
<p>冒険、戦略、アクション…全てのゲームがここに。</p>
<a href="#" class="btn">詳しく見る</a>
</div>
</div>
<div class="hero-slide" style="background-image: url('images/hero2.jpg');">
<div class="hero-content">
<h2>セール開催中!</h2>
<p>期間限定、最大50%OFF!</p>
<a href="#" class="btn">セールを見る</a>
</div>
</div>
<div class="hero-slide" style="background-image: url('images/hero3.jpg');">
<div class="hero-content">
<h2>ユーザーレビューで選ぶ</h2>
<p>実際の評価を元に、あなたにピッタリのゲームを見つけよう。</p>
<a href="#" class="btn">レビューを見る</a>
</div>
</div>
<div class="hero-controls" id="heroControls"></div>
</div>
<!--────────────────────────────
新作ゲームセクション
────────────────────────────-->
<section id="new-releases" class="fade-in">
<h2 class="section-title">新作ゲーム</h2>
<p class="section-description">最新のゲームタイトルをチェック!</p>
<div class="game-grid" id="newReleasesGrid">
<div class="game-card" data-title="新作ゲーム1" data-genre="アクション" data-price="3500円" data-description="迫力のアクションと最新技術が融合した作品。">
<img src="images/new1.jpg" alt="新作ゲーム1" loading="lazy">
<div class="game-card-content">
<h3>新作ゲーム1</h3>
<p>ジャンル: アクション</p>
<p class="price">3500円</p>
</div>
<div class="card-overlay"></div>
</div>
<div class="game-card" data-title="新作ゲーム2" data-genre="RPG" data-price="4200円" data-description="壮大な物語と美しい世界観が魅力。">
<img src="images/new2.jpg" alt="新作ゲーム2" loading="lazy">
<div class="game-card-content">
<h3>新作ゲーム2</h3>
<p>ジャンル: RPG</p>
<p class="price">4200円</p>
</div>
<div class="card-overlay"></div>
</div>
<div class="game-card" data-title="新作ゲーム3" data-genre="シューティング" data-price="2900円" data-description="高速なアクションとリアルなグラフィックが特長。">
<img src="images/new3.jpg" alt="新作ゲーム3" loading="lazy">
<div class="game-card-content">
<h3>新作ゲーム3</h3>
<p>ジャンル: シューティング</p>
<p class="price">2900円</p>
</div>
<div class="card-overlay"></div>
</div>
<div class="game-card" data-title="新作ゲーム4" data-genre="パズル" data-price="1500円" data-description="頭脳を刺激する独自のパズル体験。">
<img src="images/new4.jpg" alt="新作ゲーム4" loading="lazy">
<div class="game-card-content">
<h3>新作ゲーム4</h3>
<p>ジャンル: パズル</p>
<p class="price">1500円</p>
</div>
<div class="card-overlay"></div>
</div>
</div>
</section>
<!--────────────────────────────
注目特集セクション
────────────────────────────-->
<section id="featured" class="fade-in">
<h2 class="section-title">注目の特集</h2>
<p class="section-description">今週の注目タイトルや特集記事をお届けします。</p>
<div class="game-grid">
<div class="game-card" data-title="特集タイトル1" data-genre="特集" data-price="記事を見る" data-description="最新トレンドを徹底解説。">
<img src="images/featured1.jpg" alt="特集1" loading="lazy">
<div class="game-card-content">
<h3>特集タイトル1</h3>
<p>期間限定特集:最新トレンド</p>
<p class="price">特集記事を見る</p>
</div>
<div class="card-overlay"></div>
</div>
<div class="game-card" data-title="特集タイトル2" data-genre="特集" data-price="記事を見る" data-description="インディーゲームの革新を追う。">
<img src="images/featured2.jpg" alt="特集2" loading="lazy">
<div class="game-card-content">
<h3>特集タイトル2</h3>
<p>話題のインディーゲーム特集</p>
<p class="price">特集記事を見る</p>
</div>
<div class="card-overlay"></div>
</div>
</div>
</section>
<!--────────────────────────────
アップロード済みゲーム表示(管理者アップロードデータ)
────────────────────────────-->
<section id="my-uploads" class="fade-in">
<h2 class="section-title">アップロードしたゲーム</h2>
<p class="section-description">管理者がアップロードしたゲームデータです。</p>
<div class="game-grid" id="myUploadsGrid"></div>
</section>
<!--────────────────────────────
ジャンル別セクション
────────────────────────────-->
<section id="categories" class="fade-in">
<h2 class="section-title">ジャンル別</h2>
<p class="section-description">お気に入りのジャンルからゲームを探してみましょう。</p>
<div class="categories">
<a href="#" class="category-card">
<img src="images/cat-action.jpg" alt="アクション" loading="lazy">
<span>アクション</span>
</a>
<a href="#" class="category-card">
<img src="images/cat-rpg.jpg" alt="RPG" loading="lazy">
<span>RPG</span>
</a>
<a href="#" class="category-card">
<img src="images/cat-shooting.jpg" alt="シューティング" loading="lazy">
<span>シューティング</span>
</a>
<a href="#" class="category-card">
<img src="images/cat-puzzle.jpg" alt="パズル" loading="lazy">
<span>パズル</span>
</a>
<a href="#" class="category-card">
<img src="images/cat-adventure.jpg" alt="アドベンチャー" loading="lazy">
<span>アドベンチャー</span>
</a>
</div>
</section>
<!--────────────────────────────
ユーザーレビューセクション
────────────────────────────-->
<section id="reviews" class="fade-in">
<h2 class="section-title">ユーザーレビュー</h2>
<p class="section-description">実際にプレイしたユーザーの評価をチェック!</p>
<div class="reviews-slider">
<div class="reviews-track">
<div class="review-card">
<h4>最高のアクション体験!</h4>
<div class="rating"><span>★★★★★</span></div>
<p>アクションゲームの決定版。グラフィックも音楽も最高でした。</p>
</div>
<div class="review-card">
<h4>壮大なストーリーに引き込まれる</h4>
<div class="rating"><span>★★★★☆</span></div>
<p>RPGの世界観が素晴らしく、ストーリーにのめり込みました。</p>
</div>
<div class="review-card">
<h4>クセになる操作感!</h4>
<div class="rating"><span>★★★★★</span></div>
<p>シューティングゲームとして非常に爽快な操作感で、何度も挑戦しました。</p>
</div>
</div>
</div>
</section>
<!--────────────────────────────
ブログセクション
────────────────────────────-->
<section id="blog" class="fade-in">
<h2 class="section-title">最新ブログ</h2>
<p class="section-description">ゲーム業界の最新情報や攻略記事をお届けします。</p>
<div class="blog-grid">
<div class="blog-card">
<img src="images/blog1.jpg" alt="ブログ記事1" loading="lazy">
<div class="blog-card-content">
<h3>2025年の注目ゲームトレンド</h3>
<p>今年注目すべきゲームの傾向と今後の展望について解説します。</p>
<a href="#" class="btn">続きを読む</a>
</div>
</div>
<div class="blog-card">
<img src="images/blog2.jpg" alt="ブログ記事2" loading="lazy">
<div class="blog-card-content">
<h3>セール攻略:お得に買う方法</h3>
<p>賢く買い物するための裏技やポイントを紹介します。</p>
<a href="#" class="btn">続きを読む</a>
</div>
</div>
<div class="blog-card">
<img src="images/blog3.jpg" alt="ブログ記事3" loading="lazy">
<div class="blog-card-content">
<h3>レビューで選ぶ!おすすめタイトル</h3>
<p>実際のユーザーレビューをもとに、人気タイトルをピックアップ。</p>
<a href="#" class="btn">続きを読む</a>
</div>
</div>
</div>
</section>
<!--────────────────────────────
商品詳細モーダル
────────────────────────────-->
<div class="modal-overlay" id="productModal">
<div class="modal">
<div class="modal-header">
<h2 id="modalTitle">ゲーム詳細</h2>
<span class="modal-close" id="modalClose">×</span>
</div>
<div class="modal-body" id="modalBody">
<p id="modalDescription">詳細情報がここに表示されます。</p>
<p><strong>ジャンル:</strong> <span id="modalGenre"></span></p>
<p><strong>価格:</strong> <span id="modalPrice"></span></p>
</div>
<div class="modal-footer">
<button class="btn" id="modalBuyButton">今すぐ購入</button>
</div>
</div>
</div>
<!--────────────────────────────
ログインモーダル
────────────────────────────-->
<div class="modal-overlay" id="loginModal">
<div class="modal">
<div class="modal-header">
<h2>ログイン</h2>
<span class="modal-close" id="loginClose">×</span>
</div>
<div class="modal-body">
<form id="loginForm">
<label for="loginUsername">ユーザー名:</label>
<input type="text" id="loginUsername" required>
<label for="loginPassword">パスワード:</label>
<input type="password" id="loginPassword" required>
<button class="btn" type="submit">ログイン</button>
</form>
</div>
</div>
</div>
<!--────────────────────────────
新規登録モーダル
────────────────────────────-->
<div class="modal-overlay" id="registerModal">
<div class="modal">
<div class="modal-header">
<h2>新規登録</h2>
<span class="modal-close" id="registerClose">×</span>
</div>
<div class="modal-body">
<form id="registerForm">
<label for="registerUsername">ユーザー名:</label>
<input type="text" id="registerUsername" required>
<label for="registerEmail">メールアドレス:</label>
<input type="email" id="registerEmail" required>
<label for="registerPassword">パスワード:</label>
<input type="password" id="registerPassword" required>
<button class="btn" type="submit">登録</button>
</form>
</div>
</div>
</div>
<!--────────────────────────────
カートモーダル
────────────────────────────-->
<div class="modal-overlay" id="cartModal">
<div class="modal">
<div class="modal-header">
<h2>カート</h2>
<span class="modal-close" id="cartClose">×</span>
</div>
<div class="modal-body" id="cartBody">
<p>カートにアイテムはありません。</p>
</div>
<div class="modal-footer">
<button class="btn" id="checkoutButton">チェックアウト</button>
</div>
</div>
</div>
<!--────────────────────────────
アップロードモーダル(管理者向け)
────────────────────────────-->
<div class="modal-overlay" id="uploadModal">
<div class="modal">
<div class="modal-header">
<h2>ゲームアップロード</h2>
<span class="modal-close" id="uploadClose">×</span>
</div>
<div class="modal-body">
<form id="uploadForm" enctype="multipart/form-data">
<label for="uploadTitle">ゲームタイトル:</label>
<input type="text" id="uploadTitle" required>
<label for="uploadGenre">ジャンル:</label>
<input type="text" id="uploadGenre" required>
<label for="uploadPrice">価格:</label>
<input type="number" id="uploadPrice" required>
<label for="uploadDescription">説明:</label>
<textarea id="uploadDescription" rows="4" required></textarea>
<label for="uploadFile">画像アップロード:</label>
<input type="file" id="uploadFile" accept="image/*" required>
<button class="btn" type="submit">アップロード</button>
</form>
</div>
</div>
</div>
<!--────────────────────────────
フローティングカートボタン
────────────────────────────-->
<button id="cartButton" style="position: fixed; bottom: 100px; right: 40px; background: var(--primary); color: var(--light); padding:12px; border:none; border-radius:50%; box-shadow: var(--shadow); cursor:pointer; z-index:1500;">
</button>
<!--────────────────────────────
Back-to-Top ボタン
────────────────────────────-->
<div id="backToTop">⇧</div>
<!--────────────────────────────
JavaScript(各種機能の実装)
────────────────────────────-->
<script>
// ハンバーガーメニューの切替
const hamburger = document.getElementById('hamburger');
const navMenu = document.getElementById('navMenu');
hamburger.addEventListener('click', () => {
navMenu.classList.toggle('active');
});
// ヒーロースライダー(自動・手動切替)
const slides = document.querySelectorAll('.hero-slide');
const heroControls = document.getElementById('heroControls');
let currentSlide = 0;
slides.forEach((_, index) => {
const dot = document.createElement('div');
dot.classList.add('hero-control');
if(index === 0) dot.classList.add('active');
dot.addEventListener('click', () => showSlide(index));
heroControls.appendChild(dot);
});
function showSlide(index) {
slides[currentSlide].classList.remove('active');
heroControls.children[currentSlide].classList.remove('active');
currentSlide = index;
slides[currentSlide].classList.add('active');
heroControls.children[currentSlide].classList.add('active');
}
setInterval(() => {
let next = (currentSlide + 1) % slides.length;
showSlide(next);
}, 5000);
// Back-to-Top ボタン表示
const backToTop = document.getElementById('backToTop');
window.addEventListener('scroll', () => {
backToTop.style.display = window.scrollY > 300 ? 'block' : 'none';
});
backToTop.addEventListener('click', () => {
window.scrollTo({ top: 0, behavior: 'smooth' });
});
// IntersectionObserver によるフェードイン効果
const faders = document.querySelectorAll('.fade-in');
const appearOptions = { threshold: 0.1 };
const appearOnScroll = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if(entry.isIntersecting) {
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}
});
}, appearOptions);
faders.forEach(fader => appearOnScroll.observe(fader));
// 商品詳細モーダル
const gameCards = document.querySelectorAll('.game-card');
const productModal = document.getElementById('productModal');
const modalTitle = document.getElementById('modalTitle');
const modalDescription = document.getElementById('modalDescription');
const modalGenre = document.getElementById('modalGenre');
const modalPrice = document.getElementById('modalPrice');
const modalClose = document.getElementById('modalClose');
gameCards.forEach(card => {
card.addEventListener('click', () => {
modalTitle.textContent = card.getAttribute('data-title');
modalGenre.textContent = card.getAttribute('data-genre');
modalPrice.textContent = card.getAttribute('data-price');
modalDescription.textContent = card.getAttribute('data-description');
productModal.classList.add('active');
});
});
modalClose.addEventListener('click', () => productModal.classList.remove('active'));
productModal.addEventListener('click', (e) => { if(e.target === productModal) productModal.classList.remove('active'); });
// 検索フィルター
const searchInput = document.getElementById('searchInput');
const searchButton = document.getElementById('searchButton');
searchButton.addEventListener('click', () => {
const query = searchInput.value.toLowerCase();
const cards = document.querySelectorAll('.game-card');
cards.forEach(card => {
const title = card.getAttribute('data-title').toLowerCase();
const genre = card.getAttribute('data-genre').toLowerCase();
card.style.display = (title.includes(query) || genre.includes(query)) ? 'block' : 'none';
});
});
// フィルターサイドバー機能
const filterGenre = document.getElementById('filterGenre');
const filterPrice = document.getElementById('filterPrice');
const applyFilter = document.getElementById('applyFilter');
applyFilter.addEventListener('click', () => {
const genreValue = filterGenre.value;
const priceValue = filterPrice.value;
const cards = document.querySelectorAll('.game-card');
cards.forEach(card => {
const cardGenre = card.getAttribute('data-genre');
const cardPrice = parseInt(card.getAttribute('data-price')) || 0;
let matchesGenre = genreValue === "" || cardGenre === genreValue;
let matchesPrice = !priceValue || (cardPrice && cardPrice <= parseInt(priceValue));
card.style.display = (matchesGenre && matchesPrice) ? 'block' : 'none';
});
});
// ログインモーダル
const loginModal = document.getElementById('loginModal');
const loginClose = document.getElementById('loginClose');
loginClose.addEventListener('click', () => loginModal.classList.remove('active'));
loginModal.addEventListener('click', (e) => { if(e.target === loginModal) loginModal.classList.remove('active'); });
// 新規登録モーダル
const registerModal = document.getElementById('registerModal');
const registerClose = document.getElementById('registerClose');
registerClose.addEventListener('click', () => registerModal.classList.remove('active'));
registerModal.addEventListener('click', (e) => { if(e.target === registerModal) registerModal.classList.remove('active'); });
// カートモーダル
const cartModal = document.getElementById('cartModal');
const cartClose = document.getElementById('cartClose');
const cartButton = document.getElementById('cartButton');
cartButton.addEventListener('click', () => cartModal.classList.add('active'));
cartClose.addEventListener('click', () => cartModal.classList.remove('active'));
cartModal.addEventListener('click', (e) => { if(e.target === cartModal) cartModal.classList.remove('active'); });
// アップロードモーダル(管理者向け)
const uploadModal = document.getElementById('uploadModal');
const uploadLink = document.getElementById('uploadLink');
const uploadClose = document.getElementById('uploadClose');
uploadLink.addEventListener('click', (e) => {
e.preventDefault();
uploadModal.classList.add('active');
});
uploadClose.addEventListener('click', () => uploadModal.classList.remove('active'));
uploadModal.addEventListener('click', (e) => { if(e.target === uploadModal) uploadModal.classList.remove('active'); });
// アップロードフォーム送信(アップロードデータをlocalStorageへ保存)
const uploadForm = document.getElementById('uploadForm');
uploadForm.addEventListener('submit', (e) => {
e.preventDefault();
const title = document.getElementById('uploadTitle').value;
const genre = document.getElementById('uploadGenre').value;
const price = document.getElementById('uploadPrice').value + "円";
const description = document.getElementById('uploadDescription').value;
const fileInput = document.getElementById('uploadFile');
const file = fileInput.files[0];
if(file) {
const reader = new FileReader();
reader.onload = function(event) {
const imageData = event.target.result;
const gameData = { title, genre, price, description, image: imageData };
let uploadedGames = JSON.parse(localStorage.getItem('uploadedGames')) || [];
uploadedGames.push(gameData);
localStorage.setItem('uploadedGames', JSON.stringify(uploadedGames));
loadUploadedGames();
alert('アップロードが完了しました。');
uploadModal.classList.remove('active');
uploadForm.reset();
};
reader.readAsDataURL(file);
}
});
// アップロード済みゲームを読み込み表示する(「アップロードしたゲーム」セクション)
function loadUploadedGames() {
const grid = document.getElementById('myUploadsGrid');
let uploadedGames = JSON.parse(localStorage.getItem('uploadedGames')) || [];
grid.innerHTML = "";
uploadedGames.forEach(game => {
const card = document.createElement('div');
card.classList.add('game-card');
card.setAttribute('data-title', game.title);
card.setAttribute('data-genre', game.genre);
card.setAttribute('data-price', game.price);
card.setAttribute('data-description', game.description);
card.innerHTML = `
<img src="${game.image}" alt="${game.title}" loading="lazy">
<div class="game-card-content">
<h3>${game.title}</h3>
<p>ジャンル: ${game.genre}</p>
<p class="price">${game.price}</p>
</div>
<div class="card-overlay"></div>
`;
card.addEventListener('click', () => {
modalTitle.textContent = game.title;
modalGenre.textContent = game.genre;
modalPrice.textContent = game.price;
modalDescription.textContent = game.description;
productModal.classList.add('active');
});
grid.appendChild(card);
});
}
// ページ読み込み時にアップロード済みゲームを表示
loadUploadedGames();
</script>
</body>
<footer>
<div class="footer-container">
<div class="footer-col">
<h4>会社情報</h4>
<ul>
<li><a href="#">企業理念</a></li>
<li><a href="#">採用情報</a></li>
<li><a href="#">プレスリリース</a></li>
</ul>
</div>
<div class="footer-col">
<h4>サポート</h4>
<ul>
<li><a href="#">お問い合わせ</a></li>
<li><a href="#">FAQ</a></li>
<li><a href="#">利用規約</a></li>
</ul>
</div>
<div class="footer-col">
<h4>ニュースレター</h4>
<p>最新情報をメールで受け取る</p>
<form class="subscribe-form">
<input type="email" placeholder="メールアドレス" required>
<button type="submit">登録</button>
</form>
</div>
</div>
<div class="footer-bottom">© 2025 GameStore Ultra. All rights reserved.</div>
</footer>
</html>
QuestifyInfinity.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Questify Advanced Battle</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- レトロゲーム風フォント -->
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
<style>
/* ========== 全体 ========== */
body {
background-color: #1a1a1a;
color: #fff;
font-family: 'Press Start 2P', cursive;
margin: 0; padding: 0;
}
header, footer {
background: #333; text-align: center; padding: 15px; border-bottom: 2px solid #555;
}
header h1 { font-size: 1.3rem; margin: 0; }
main { max-width: 1200px; margin: 10px auto; padding-bottom: 80px; }
.box {
background: #2b2b2b; margin-bottom: 20px;
border: 2px solid #555; padding: 20px; position: relative;
}
.section-title { font-size: 1.2rem; margin-bottom: 10px; }
button {
font-family: 'Press Start 2P', cursive;
border: none; cursor: pointer; padding: 6px 10px; margin-right: 5px;
}
button:hover { opacity: 0.8; }
.btn-primary { background: #006699; color: #fff; }
.btn-delete { background: #bb3333; color: #fff; }
/* ステータス表示 */
.hero-info p { margin: 5px 0; }
.xp-bar {
background: #555; width: 100%; height: 20px; margin: 5px 0;
position: relative; overflow: hidden;
}
.xp-fill {
background: #00aa00; width: 0%; height: 100%;
transition: width 0.3s ease;
}
/* パーティ */
.party-member {
background: #333; border: 2px solid #555;
padding: 10px; margin-bottom: 10px;
}
/* スキルツリー */
.skill-tree .skill {
background: #333; border: 2px solid #555;
padding: 10px; margin-bottom: 10px;
}
/* 装備・素材 */
.equipment-info p { margin: 3px 0; }
.craft-recipe {
border: 1px dashed #999;
margin-bottom: 10px; padding: 5px;
}
/* クエスト */
.quest-list .quest {
background: #333; border: 2px solid #555;
padding: 10px; margin-bottom: 10px;
}
/* 実績 */
.achievements .achievement {
background: #333; border: 2px solid #555;
padding: 10px; margin-bottom: 10px; position: relative;
}
.achievement.locked { opacity: 0.5; }
.locked-label {
position: absolute; top: 5px; right: 5px;
background: #cc0000; padding: 3px 5px; font-size: 0.7rem;
}
/* バトルモーダル */
.modal-bg {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.8);
display: none; justify-content: center; align-items: center;
}
.modal {
background: #2b2b2b; border: 2px solid #555;
padding: 20px; max-width: 600px; width: 90%;
position: relative;
}
.close-btn {
position: absolute; top: 10px; right: 10px;
background: #dd3333; color: #fff; padding: 5px 8px; border: none;
}
.battle-enemy-info, .battle-hero-info {
margin-bottom: 10px;
}
#battleLog {
border: 1px solid #555;
min-height: 60px; padding: 5px;
margin: 10px 0; max-height: 200px; overflow-y: auto;
}
.battle-action-buttons { margin-top: 10px; }
.battle-action-buttons button { margin: 5px 5px 0 0; }
</style>
</head>
<body>
<header>
<h1>Questify Advanced Battle</h1>
</header>
<main>
<!-- ===================== ヒーロー & パーティステータス ===================== -->
<section class="box">
<h2 class="section-title">冒険者ステータス</h2>
<div class="hero-info">
<p>勇者: <span id="heroNameDisplay">No Name</span></p>
<p>職業: <span id="heroJobDisplay">未設定</span></p>
<p>
Lv.<span id="heroLevel">1</span>
HP:<span id="heroHp">?</span>/<span id="heroMaxHp">?</span>
</p>
<div class="xp-bar">
<div class="xp-fill" id="xpFill"></div>
</div>
<p>EXP: <span id="heroXp">0</span> / <span id="xpToNextLevel">100</span></p>
<p>Gold: <span id="heroGold">0</span></p>
<p>Skill Pts: <span id="heroSkillPts">0</span></p>
<label>勇者名: <input type="text" id="heroNameInput" placeholder="アルス" /></label>
<button onclick="changeHeroName()">変更</button>
</div>
</section>
<section class="box">
<h2 class="section-title">パーティメンバー</h2>
<div id="partyList"></div>
<button class="btn-primary" onclick="addPartyMember()">仲間を雇う (最大2人)</button>
</section>
<!-- ===================== スキルツリー ===================== -->
<section class="box">
<h2 class="section-title">職業 & スキルツリー</h2>
<p>
<button onclick="setJob('戦士')">戦士</button>
<button onclick="setJob('魔法使い')">魔法使い</button>
<button onclick="setJob('盗賊')">盗賊</button>
</p>
<div class="skill-tree" id="skillTree"></div>
</section>
<!-- ===================== 装備 & クラフト ===================== -->
<section class="box">
<h2 class="section-title">装備 & クラフト</h2>
<div class="equipment-info" id="heroEquipment"></div>
<h3>クラフトレシピ</h3>
<div id="craftContainer"></div>
</section>
<!-- ===================== クエスト (デイリー/通常/ウィークリー) ===================== -->
<section class="box">
<h2 class="section-title">クエスト</h2>
<div style="margin-bottom:10px;">
<button class="btn-primary" onclick="switchQuestTab('daily')">デイリー</button>
<button class="btn-primary" onclick="switchQuestTab('normal')">通常</button>
<button class="btn-primary" onclick="switchQuestTab('weekly')">ウィークリー</button>
</div>
<div id="dailyQuests" class="quest-list"></div>
<div id="normalQuests" class="quest-list" style="display:none;"></div>
<div id="weeklyQuests" class="quest-list" style="display:none;"></div>
</section>
<!-- ===================== マップ & ボス戦 ===================== -->
<section class="box">
<h2 class="section-title">ワールドマップ</h2>
<div id="mapAreaContainer"></div>
</section>
<!-- ===================== ランダムエンカウント ===================== -->
<section class="box">
<h2 class="section-title">ダンジョン潜入 (複数敵ランダムエンカウント)</h2>
<p>ボタンを押すと **複数の敵** が出る場合も!</p>
<button class="btn-primary" onclick="startRandomEncounter()">ダンジョンに潜る</button>
</section>
<!-- ===================== 実績 & ストーリー ===================== -->
<section class="box achievements">
<h2 class="section-title">実績(Achievements)</h2>
<div id="achievementList"></div>
</section>
<section class="box">
<h2 class="section-title">ストーリー進行</h2>
<div id="storyProgress"></div>
</section>
</main>
<footer>
<p>© 2025 Questify Advanced Battle</p>
</footer>
<!-- ========== 個別ターン制バトルモーダル ========== -->
<div class="modal-bg" id="battleModalBg">
<div class="modal" id="battleModal">
<button class="close-btn" onclick="closeBattleModal()">×</button>
<h2 id="battleTitle">バトル</h2>
<p id="battleDesc"></p>
<!-- 敵一覧表示 -->
<div class="battle-enemy-info" id="battleEnemyInfo"></div>
<!-- 味方一覧表示 -->
<div class="battle-hero-info" id="battleHeroInfo"></div>
<!-- 行動ログ -->
<div id="battleLog"></div>
<!-- プレイヤー操作ボタン (アクション選択) -->
<div class="battle-action-buttons" id="battleActionButtons">
<button onclick="chooseAttack()">攻撃</button>
<button onclick="chooseSkill()">スキル</button>
<button onclick="chooseItem()">アイテム</button>
<button onclick="chooseDefend()">防御</button>
<button onclick="chooseFlee()">逃げる</button>
</div>
</div>
</div>
<script>
/* =========================================
1) データ構造 & ローカルストレージ
========================================= */
const STORAGE_KEY = "questify_battle_data";
let gameData = {
hero: {
name: "No Name",
job: "未設定",
level: 1,
xp: 0,
gold: 0,
skillPts: 0,
hp: 50,
maxHp: 50,
speed: 8, // 追加: 素早さ
equipment: { weapon: null, armor: null, accessory: null },
materials: { wood: 0, ore: 0, magicCrystal: 0 },
consumables: { potion: 2 },
// アクティブスキル(例): 攻撃スキル
activeSkills: [
{ id: "slash", name: "パワースラッシュ", baseDamage: 15 },
// ここに追加スキルを増やす
]
},
party: [
// 仲間も speed を持つ、activeSkills を持つなど拡張可能
// { id, name, level, xp, hp, maxHp, speed, attack, equipment, activeSkills, ... }
],
// 職業ごとのパッシブスキル
skills: {
warrior: [
{ id: 1, name: "剣術熟練", level: 0, maxLevel: 5, desc: "攻撃力+2/Lv" },
{ id: 2, name: "体力増強", level: 0, maxLevel: 5, desc: "HP+10/Lv" }
],
mage: [
{ id: 1, name: "魔力増強", level: 0, maxLevel: 5, desc: "魔法攻撃力+3/Lv" },
{ id: 2, name: "精神集中", level: 0, maxLevel: 5, desc: "ボス戦追加ダメージ+5/Lv" }
],
thief: [
{ id: 1, name: "素早さ強化", level: 0, maxLevel: 5, desc: "攻撃力+2/Lv" },
{ id: 2, name: "ゴールド盗み", level: 0, maxLevel: 5, desc: "討伐時Gold+5/Lv" }
]
},
// クラフトレシピ & クエスト & マップ & 実績 等は前回コードと同じ
// …(省略なし、全て記載)…
craftingRecipes: [
{
id: 1,
result: { name: "回復薬", type: "consumable", itemKey: "potion", amount: 1, hpRestore: 30 },
materialsRequired: { wood: 1, magicCrystal: 1 },
desc: "木材1 & 魔力結晶1 で回復薬を1つ生成"
}
],
dailyQuests: [
{ id: 1, title: "【デイリー】部屋掃除", exp: 10, gold: 5, completed: false },
{ id: 2, title: "【デイリー】筋トレ15分", exp: 15, gold: 5, completed: false }
],
normalQuests: [
{ id: 1, title: "HTML/CSSの学習", exp: 20, gold: 10, completed: false },
{ id: 2, title: "ランニング30分", exp: 25, gold: 10, completed: false }
],
weeklyQuests: [
{ id: 1, title: "【ウィークリー】5日連続早起き", exp: 50, gold: 30, completed: false },
{ id: 2, title: "【ウィークリー】合計5時間の学習", exp: 60, gold: 40, completed: false }
],
mapAreas: [
{ id: 1, name: "街", levelReq: 1, desc: "安全な街", boss: null },
{ id: 2, name: "森", levelReq: 3, desc: "木材が手に入るかも", boss: { name: "森の主", hp: 60, maxHp: 60, speed: 6, atk: 12, rewardExp: 50, rewardGold: 30 } },
{ id: 3, name: "洞窟", levelReq: 5, desc: "鉱石が眠る", boss: { name: "ゴーレム", hp: 100, maxHp: 100, speed: 4, atk: 20, rewardExp: 100, rewardGold: 80 } },
{ id: 4, name: "魔王城", levelReq: 10, desc: "魔王が支配する城", boss: { name: "魔王", hp: 200, maxHp: 200, speed: 10, atk: 35, rewardExp: 300, rewardGold: 200 } }
],
achievements: [
{ id: 1, title: "初クエスト達成", desc: "クエストを1回完了", type: "questCount", target: 1, unlocked: false },
{ id: 2, title: "レベル10到達", desc: "Lv.10になる", type: "level", target: 10, unlocked: false },
{ id: 3, title: "森の主撃破", desc: "森の主を倒す", type: "bossKill", bossName: "森の主", unlocked: false },
{ id: 4, title: "魔王撃破", desc: "魔王を倒す", type: "bossKill", bossName: "魔王", unlocked: false },
{ id: 5, title: "金持ち", desc: "ゴールドが100を超える", type: "gold", target: 100, unlocked: false }
],
story: [
{ bossName: "森の主", text: "森の主を倒し、森に平穏が戻った…!" },
{ bossName: "ゴーレム", text: "洞窟のゴーレムを粉砕し、鉱山への道が開けた。" },
{ bossName: "魔王", text: "魔王を倒し、世界に平和が訪れた。あなたは真の勇者!" }
],
enemies: [
{ name: "森の狼", hp: 30, maxHp: 30, speed: 7, atk: 8, exp: 15, gold: 5, dropMat: { wood: 1 } },
{ name: "洞窟コウモリ", hp: 35, maxHp: 35, speed: 9, atk: 10, exp: 20, gold: 8, dropMat: { ore: 1 } },
{ name: "ゴブリン", hp: 40, maxHp: 40, speed: 5, atk: 12, exp: 25, gold: 10, dropMat: { wood: 1, ore: 1 } }
],
lastDailyReset: null,
lastWeeklyReset: null
};
function loadData() {
const saved = localStorage.getItem(STORAGE_KEY);
if (saved) {
gameData = JSON.parse(saved);
}
}
function saveData() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(gameData));
}
/* =========================================
2) 初期化処理
========================================= */
window.addEventListener("load", () => {
loadData();
dailyQuestResetCheck();
weeklyQuestResetCheck();
applySkillPassive();
updateAllUI();
});
/* =========================================
3) ユーティリティ & ステータス系
========================================= */
function xpNeededForLevel(level) {
return level * 100;
}
function checkLevelUp() {
while (gameData.hero.xp >= xpNeededForLevel(gameData.hero.level)) {
gameData.hero.xp -= xpNeededForLevel(gameData.hero.level);
gameData.hero.level++;
gameData.hero.skillPts++;
applySkillPassive();
alert(`レベルアップ!Lv.${gameData.hero.level} になった。スキルポイント+1`);
}
}
function gainExp(amount) {
gameData.hero.xp += amount;
checkLevelUp();
saveData();
updateHeroUI();
checkAchievements();
}
function gainGold(amount) {
gameData.hero.gold += amount;
saveData();
updateHeroUI();
checkAchievements();
}
function updateHeroUI() {
const h = gameData.hero;
document.getElementById("heroNameDisplay").textContent = h.name;
document.getElementById("heroJobDisplay").textContent = h.job;
document.getElementById("heroLevel").textContent = h.level;
document.getElementById("heroHp").textContent = h.hp;
document.getElementById("heroMaxHp").textContent = h.maxHp;
document.getElementById("heroXp").textContent = h.xp;
document.getElementById("xpToNextLevel").textContent = xpNeededForLevel(h.level);
document.getElementById("heroGold").textContent = h.gold;
document.getElementById("heroSkillPts").textContent = h.skillPts;
const ratio = (h.xp / xpNeededForLevel(h.level)) * 100;
document.getElementById("xpFill").style.width = ratio + "%";
updateHeroEquipmentUI();
}
function changeHeroName() {
const input = document.getElementById("heroNameInput");
const newName = input.value.trim();
if (!newName) return;
gameData.hero.name = newName;
input.value = "";
saveData();
updateHeroUI();
}
function applySkillPassive() {
const hero = gameData.hero;
hero.maxHp = 50 + (hero.level - 1) * 5;
let skillList = [];
if (hero.job === "戦士") skillList = gameData.skills.warrior;
if (hero.job === "魔法使い") skillList = gameData.skills.mage;
if (hero.job === "盗賊") skillList = gameData.skills.thief;
skillList.forEach(s => {
if (s.name === "体力増強") {
hero.maxHp += (s.level * 10);
}
});
if (hero.hp > hero.maxHp) hero.hp = hero.maxHp;
}
/* =========================================
4) パーティ管理
========================================= */
function addPartyMember() {
if (gameData.party.length >= 2) {
alert("仲間は最大2人までです。");
return;
}
const newMem = {
id: Date.now(),
name: `仲間${gameData.party.length + 1}`,
level: 1,
xp: 0,
hp: 40,
maxHp: 40,
speed: 5,
attack: 5,
equipment: { weapon: null, armor: null, accessory: null },
activeSkills: [
// 例: 仲間専用スキルを入れたければここに
]
};
gameData.party.push(newMem);
alert(`${newMem.name} を雇いました!`);
saveData();
updateAllUI();
}
function updatePartyUI() {
const container = document.getElementById("partyList");
container.innerHTML = "";
if (gameData.party.length === 0) {
container.innerHTML = "<p>仲間はいません</p>";
return;
}
gameData.party.forEach(m => {
const div = document.createElement("div");
div.className = "party-member";
div.innerHTML = `<p>${m.name} (Lv.${m.level}) HP:${m.hp}/${m.maxHp} 攻:${m.attack}</p>`;
const leaveBtn = document.createElement("button");
leaveBtn.className = "btn-delete";
leaveBtn.textContent = "離脱";
leaveBtn.onclick = () => {
if (confirm(`${m.name}を外しますか?`)) {
gameData.party = gameData.party.filter(x => x.id !== m.id);
saveData();
updateAllUI();
}
};
div.appendChild(leaveBtn);
container.appendChild(div);
});
}
function distributePartyExp(amount) {
gameData.party.forEach(m => {
m.xp += amount;
while (m.xp >= m.level * 50) {
m.xp -= m.level * 50;
m.level++;
m.attack += 2;
m.maxHp += 5;
m.hp += 5;
}
});
}
/* =========================================
5) 職業 & スキルツリー
========================================= */
function setJob(job) {
gameData.hero.job = job;
alert(`職業を「${job}」に変更しました。`);
saveData();
applySkillPassive();
updateAllUI();
}
function updateSkillTreeUI() {
const container = document.getElementById("skillTree");
container.innerHTML = "";
const job = gameData.hero.job;
if (job === "未設定") {
container.innerHTML = "<p>職業を選択してください</p>";
return;
}
let skillList = [];
if (job === "戦士") skillList = gameData.skills.warrior;
if (job === "魔法使い") skillList = gameData.skills.mage;
if (job === "盗賊") skillList = gameData.skills.thief;
skillList.forEach(s => {
const skillDiv = document.createElement("div");
skillDiv.className = "skill";
skillDiv.innerHTML = `
<h4>${s.name} (Lv.${s.level}/${s.maxLevel})</h4>
<p>${s.desc}</p>
`;
const btn = document.createElement("button");
btn.className = "btn-primary";
if (s.level >= s.maxLevel) {
btn.textContent = "MAX";
btn.disabled = true;
} else {
btn.textContent = "強化";
btn.onclick = () => {
if (gameData.hero.skillPts <= 0) {
alert("スキルポイントが足りません");
return;
}
s.level++;
gameData.hero.skillPts--;
saveData();
applySkillPassive();
updateAllUI();
};
}
skillDiv.appendChild(btn);
container.appendChild(skillDiv);
});
}
/* =========================================
6) 装備 & クラフト(簡易化)
========================================= */
function updateHeroEquipmentUI() {
const eqDiv = document.getElementById("heroEquipment");
const h = gameData.hero;
eqDiv.innerHTML = `
<p>武器: ${h.equipment.weapon ? h.equipment.weapon.name : "なし"}</p>
<p>防具: ${h.equipment.armor ? h.equipment.armor.name : "なし"}</p>
<p>アクセ: ${h.equipment.accessory ? h.equipment.accessory.name : "なし"}</p>
<p>素材: 木材(${h.materials.wood}), 鉱石(${h.materials.ore}), 結晶(${h.materials.magicCrystal})</p>
<p>回復薬: ${h.consumables.potion || 0}個</p>
`;
}
function updateCraftUI() {
const cDiv = document.getElementById("craftContainer");
cDiv.innerHTML = "";
gameData.craftingRecipes.forEach(r => {
const div = document.createElement("div");
div.className = "craft-recipe";
let matText = Object.keys(r.materialsRequired).map(m => {
return `${m}:${r.materialsRequired[m]}`;
}).join(", ");
div.innerHTML = `
<strong>${r.result.name}</strong> → 必要素材 ${matText}<br>
${r.desc}
`;
const btn = document.createElement("button");
btn.textContent = "クラフト";
btn.onclick = () => craftItem(r);
div.appendChild(btn);
cDiv.appendChild(div);
});
}
function craftItem(recipe) {
for (let mat in recipe.materialsRequired) {
if ((gameData.hero.materials[mat] || 0) < recipe.materialsRequired[mat]) {
alert("素材が足りません!");
return;
}
}
for (let mat in recipe.materialsRequired) {
gameData.hero.materials[mat] -= recipe.materialsRequired[mat];
}
if (recipe.result.type === "consumable") {
const key = recipe.result.itemKey;
gameData.hero.consumables[key] = (gameData.hero.consumables[key] || 0) + recipe.result.amount;
alert(`${recipe.result.name} を${recipe.result.amount}個、作成しました!`);
}
saveData();
updateAllUI();
}
/* =========================================
7) クエスト関連 (デイリー/通常/ウィークリー)
========================================= */
function switchQuestTab(tab) {
document.getElementById("dailyQuests").style.display = (tab === "daily") ? "" : "none";
document.getElementById("normalQuests").style.display = (tab === "normal") ? "" : "none";
document.getElementById("weeklyQuests").style.display = (tab === "weekly") ? "" : "none";
}
function updateQuestsUI() {
renderQuestList("dailyQuests", gameData.dailyQuests, completeDailyQuest);
renderQuestList("normalQuests", gameData.normalQuests, completeNormalQuest);
renderQuestList("weeklyQuests", gameData.weeklyQuests, completeWeeklyQuest);
}
function renderQuestList(containerId, questArr, completeFn) {
const container = document.getElementById(containerId);
container.innerHTML = "";
questArr.forEach(q => {
const div = document.createElement("div");
div.className = "quest";
const h4 = document.createElement("h4");
h4.textContent = q.completed ? `【達成済】${q.title}` : q.title;
const p = document.createElement("p");
p.textContent = `EXP:${q.exp} Gold:${q.gold}`;
const btn = document.createElement("button");
btn.className = "btn-primary";
if (q.completed) {
btn.textContent = "完了済";
btn.disabled = true;
} else {
btn.textContent = "達成";
btn.onclick = () => completeFn(q.id);
}
div.appendChild(h4);
div.appendChild(p);
div.appendChild(btn);
container.appendChild(div);
});
}
function completeDailyQuest(id) {
const q = gameData.dailyQuests.find(x => x.id === id);
if (!q || q.completed) return;
q.completed = true;
alert(`${q.title} を達成!\nEXP+${q.exp}, Gold+${q.gold}`);
gainExp(q.exp);
gainGold(q.gold);
saveData();
updateAllUI();
}
function completeNormalQuest(id) {
const q = gameData.normalQuests.find(x => x.id === id);
if (!q || q.completed) return;
q.completed = true;
alert(`${q.title} を達成!\nEXP+${q.exp}, Gold+${q.gold}`);
gainExp(q.exp);
gainGold(q.gold);
saveData();
updateAllUI();
}
function completeWeeklyQuest(id) {
const q = gameData.weeklyQuests.find(x => x.id === id);
if (!q || q.completed) return;
q.completed = true;
alert(`${q.title} を達成!\nEXP+${q.exp}, Gold+${q.gold}`);
gainExp(q.exp);
gainGold(q.gold);
saveData();
updateAllUI();
}
function dailyQuestResetCheck() {
const now = new Date();
const todayStr = now.toDateString();
if (gameData.lastDailyReset !== todayStr) {
gameData.dailyQuests.forEach(q => q.completed = false);
gameData.lastDailyReset = todayStr;
alert("デイリークエストをリセットしました!");
saveData();
}
}
function weeklyQuestResetCheck() {
const now = new Date();
const year = now.getFullYear();
const weekNum = Math.floor((now.getDate() - now.getDay() + 10) / 7);
const currentWeekKey = `${year}-W${weekNum}`;
if (gameData.lastWeeklyReset !== currentWeekKey) {
gameData.weeklyQuests.forEach(q => q.completed = false);
gameData.lastWeeklyReset = currentWeekKey;
alert("ウィークリークエストをリセットしました!");
saveData();
}
}
/* =========================================
8) ワールドマップ & ボス(複数ターン制)
========================================= */
function updateMapUI() {
const container = document.getElementById("mapAreaContainer");
container.innerHTML = "";
const heroLevel = gameData.hero.level;
gameData.mapAreas.forEach(area => {
const areaDiv = document.createElement("div");
areaDiv.className = "map-area";
if (heroLevel < area.levelReq) {
areaDiv.classList.add("locked-area");
}
areaDiv.innerHTML = `<h3>${area.name} (Lv.${area.levelReq}~)</h3><p>${area.desc}</p>`;
if (area.boss && heroLevel >= area.levelReq) {
const btn = document.createElement("button");
btn.className = "btn-primary";
btn.textContent = `${area.boss.name} と戦う`;
btn.onclick = () => {
// ボスも enemies配列扱い
const boss = JSON.parse(JSON.stringify(area.boss));
startBattle([boss], `ボス戦: ${boss.name}`, true);
};
areaDiv.appendChild(btn);
}
container.appendChild(areaDiv);
});
}
/* =========================================
9) ランダムエンカウント (複数敵対応)
========================================= */
function startRandomEncounter() {
// ランダムに1~2体の敵を出す
const n = Math.random() < 0.5 ? 1 : 2;
let chosenEnemies = [];
for (let i = 0; i < n; i++) {
const e = gameData.enemies[Math.floor(Math.random() * gameData.enemies.length)];
chosenEnemies.push(JSON.parse(JSON.stringify(e)));
}
startBattle(chosenEnemies, "ダンジョン潜入 - 複数敵が出現!");
}
/* =========================================
10) 個別ターン制バトルロジック
========================================= */
let battleState = {
combatants: [], // 全ユニット(勇者/仲間/敵)
turnIndex: 0,
log: [],
battleOver: false,
isBossFight: false
};
function startBattle(enemyList, title, isBoss=false) {
battleState.combatants = [];
battleState.turnIndex = 0;
battleState.log = [];
battleState.battleOver = false;
battleState.isBossFight = isBoss;
// 味方(勇者)
const hero = gameData.hero;
battleState.combatants.push({
unitType: "hero",
name: hero.name,
hp: hero.hp,
maxHp: hero.maxHp,
speed: hero.speed,
isDown: false,
ref: hero,
activeSkills: hero.activeSkills || []
});
// 仲間
gameData.party.forEach(m => {
battleState.combatants.push({
unitType: "party",
name: m.name,
hp: m.hp,
maxHp: m.maxHp,
speed: m.speed || 5,
isDown: false,
ref: m,
activeSkills: m.activeSkills || []
});
});
// 敵
enemyList.forEach(e => {
battleState.combatants.push({
unitType: "enemy",
name: e.name,
hp: e.hp,
maxHp: e.maxHp,
speed: e.speed || 5,
atk: e.atk || 5,
exp: e.exp || 0,
gold: e.gold || 0,
dropMat: e.dropMat || null,
rewardExp: e.rewardExp,
rewardGold: e.rewardGold,
isDown: false
});
});
// ソート(速度降順)
battleState.combatants.sort((a,b) => b.speed - a.speed);
document.getElementById("battleTitle").textContent = title;
document.getElementById("battleDesc").textContent = isBoss ? "ボス戦だ!" : "敵が現れた!";
openBattleModal();
updateBattleUI();
battleState.log.push("バトル開始!");
// 行動者チェック
checkCurrentTurn();
}
function openBattleModal() {
document.getElementById("battleModalBg").style.display = "flex";
}
function closeBattleModal() {
document.getElementById("battleModalBg").style.display = "none";
updateAllUI();
}
function updateBattleUI() {
// 敵一覧
let enemyText = "<h3>敵ユニット</h3>";
battleState.combatants
.filter(c => c.unitType === "enemy")
.forEach(c => {
if (!c.isDown) {
enemyText += `<p>${c.name} HP:${c.hp}/${c.maxHp}</p>`;
} else {
enemyText += `<p>${c.name} (撃破)</p>`;
}
});
document.getElementById("battleEnemyInfo").innerHTML = enemyText;
// 味方一覧
let allyText = "<h3>味方ユニット</h3>";
battleState.combatants
.filter(c => c.unitType==="hero" || c.unitType==="party")
.forEach(c => {
if (!c.isDown) {
allyText += `<p>${c.name} HP:${c.hp}/${c.maxHp}</p>`;
} else {
allyText += `<p>${c.name} (戦闘不能)</p>`;
}
});
document.getElementById("battleHeroInfo").innerHTML = allyText;
// ログ
document.getElementById("battleLog").innerHTML = battleState.log.join("<br>");
}
/* 行動者確認 */
function checkCurrentTurn() {
if (battleState.battleOver) return;
if (battleState.turnIndex >= battleState.combatants.length) {
battleState.turnIndex = 0;
}
let currentUnit = battleState.combatants[battleState.turnIndex];
if (currentUnit.isDown) {
// 次へ
battleState.turnIndex++;
checkBattleEnd();
checkCurrentTurn();
return;
}
if (currentUnit.unitType === "hero" || currentUnit.unitType === "party") {
// プレイヤー(味方)行動
battleState.log.push(`[${currentUnit.name} のターン]`);
updateBattleUI();
// ボタン操作で行動を待つ
} else {
// 敵行動
battleState.log.push(`[${currentUnit.name} のターン(敵)]`);
updateBattleUI();
setTimeout(() => {
enemyAction(currentUnit);
}, 600);
}
}
/* プレイヤー行動系 */
// 今どのキャラが行動中か
function getCurrentUnit() {
if (battleState.turnIndex < battleState.combatants.length) {
return battleState.combatants[battleState.turnIndex];
}
return null;
}
// 攻撃
function chooseAttack() {
const currentUnit = getCurrentUnit();
if (!currentUnit) return;
if (currentUnit.unitType==="enemy") return; // 敵は自動行動
// ターゲット: 生存している敵
const aliveEnemies = battleState.combatants.filter(c => c.unitType==="enemy" && !c.isDown);
if (aliveEnemies.length === 0) return; // 敵がいない
const target = aliveEnemies[0]; // 仮に先頭を攻撃(本当は選択UIを用意してもOK)
doAttack(currentUnit, target);
}
function doAttack(attacker, defender) {
battleState.log.push(`${attacker.name}の攻撃!`);
const dmg = calcDamage(attacker, defender, false);
defender.hp -= dmg;
battleState.log.push(`→ ${defender.name} に ${dmg} ダメージ!`);
if (defender.hp<=0) {
defender.hp=0;
defender.isDown=true;
battleState.log.push(`${defender.name}は倒れた!`);
}
endPlayerAction();
}
// スキル
function chooseSkill() {
const currentUnit = getCurrentUnit();
if (!currentUnit) return;
if (currentUnit.activeSkills.length === 0) {
alert("スキルがありません!");
return;
}
// 例: 1つだけスキルがあるなら即使用 or promptで選択
const skill = currentUnit.activeSkills[0];
// ターゲット選択(敵)
const aliveEnemies = battleState.combatants.filter(c => c.unitType==="enemy" && !c.isDown);
if (aliveEnemies.length === 0) {
alert("敵がいません");
return;
}
const target = aliveEnemies[0]; // 簡易: 先頭
battleState.log.push(`${currentUnit.name}は${skill.name}を発動!`);
const dmg = skill.baseDamage + Math.floor(Math.random()*3);
target.hp -= dmg;
battleState.log.push(`→ ${target.name} に ${dmg} ダメージ!`);
if (target.hp<=0) {
target.hp=0;
target.isDown=true;
battleState.log.push(`${target.name}は倒れた!`);
}
endPlayerAction();
}
// アイテム
function chooseItem() {
const currentUnit = getCurrentUnit();
if (!currentUnit) return;
if (currentUnit.unitType!=="hero") {
alert("このキャラはアイテムを使えません。");
return;
}
const hero = currentUnit.ref;
if ((hero.consumables.potion || 0) <1) {
alert("回復薬がありません。");
return;
}
// 対象を自分に限定(簡易)
hero.consumables.potion--;
const heal=30;
currentUnit.hp += heal;
if(currentUnit.hp>currentUnit.maxHp) currentUnit.hp=currentUnit.maxHp;
battleState.log.push(`${currentUnit.name}は回復薬を使用 → HP+${heal}`);
endPlayerAction();
}
// 防御
function chooseDefend() {
const currentUnit = getCurrentUnit();
if (!currentUnit) return;
currentUnit.isDefending=true; // 被ダメ半減など
battleState.log.push(`${currentUnit.name}は身を守っている!(被ダメ軽減)`);
endPlayerAction();
}
// 逃げる
function chooseFlee() {
battleState.log.push("逃げ出した!");
endBattle(false);
}
// プレイヤー行動終了
function endPlayerAction() {
battleState.turnIndex++;
checkBattleEnd();
setTimeout(() => {
checkCurrentTurn();
updateBattleUI();
}, 400);
}
// 敵行動
function enemyAction(enemyUnit) {
// ターゲット: 生存している味方( hero/party )
const aliveAllies = battleState.combatants.filter(c => (c.unitType==="hero"||c.unitType==="party") && !c.isDown);
if (aliveAllies.length===0) {
checkBattleEnd();
return;
}
const target = aliveAllies[Math.floor(Math.random()*aliveAllies.length)];
battleState.log.push(`${enemyUnit.name}の攻撃 → ${target.name}`);
const dmg = calcDamage(enemyUnit, target, target.isDefending);
target.hp -= dmg;
if (target.isDefending) {
battleState.log.push("(防御中で被ダメ軽減)");
}
battleState.log.push(`→ ${target.name}に${dmg}ダメージ!`);
target.isDefending=false; // 防御は1ターンのみ
if (target.hp<=0) {
target.hp=0;
target.isDown=true;
battleState.log.push(`${target.name}は倒れた…`);
}
battleState.turnIndex++;
checkBattleEnd();
setTimeout(() => {
checkCurrentTurn();
updateBattleUI();
}, 400);
}
/* ダメージ計算 */
function calcDamage(attacker, defender, defenderIsDefending) {
let baseAtk=0;
if (attacker.unitType==="hero"||attacker.unitType==="party") {
// 味方の攻撃力
// (装備や職業スキルなどは未細分化。下のcalcPlayerTotalAttackを簡易流用でもOK)
baseAtk=10;
if(attacker.ref && attacker.ref.job==="戦士") {
const wSkills=gameData.skills.warrior;
wSkills.forEach(s => {
if(s.name==="剣術熟練") baseAtk+=(s.level*2);
});
}
if(attacker.ref && attacker.ref.job==="魔法使い") {
const mSkills=gameData.skills.mage;
mSkills.forEach(s => {
if(s.name==="魔力増強") baseAtk+=(s.level*3);
if(s.name==="精神集中" && battleState.isBossFight) baseAtk+=(s.level*5);
});
}
if(attacker.ref && attacker.ref.job==="盗賊") {
const tSkills=gameData.skills.thief;
tSkills.forEach(s=>{
if(s.name==="素早さ強化") baseAtk+=(s.level*2);
});
}
// 装備
if(attacker.ref && attacker.ref.equipment.weapon) {
baseAtk += (attacker.ref.equipment.weapon.attack||0);
}
if(attacker.ref && attacker.ref.equipment.armor) {
baseAtk += (attacker.ref.equipment.armor.attack||0);
}
if(attacker.ref && attacker.ref.equipment.accessory) {
baseAtk += (attacker.ref.equipment.accessory.attack||0);
}
// さらに仲間の場合はattackプロパティ?
if(attacker.unitType==="party") {
baseAtk += attacker.ref.attack; // m.attack
}
} else {
// 敵
baseAtk=attacker.atk||5;
}
// 防御中なら半減
let finalDmg = baseAtk + Math.floor(Math.random()*3);
if(defenderIsDefending) {
finalDmg = Math.floor(finalDmg/2);
}
return finalDmg;
}
/* 勝利/敗北判定 */
function checkBattleEnd() {
// 味方生存
const aliveAllies = battleState.combatants.filter(c => (c.unitType==="hero"||c.unitType==="party") && !c.isDown);
if(aliveAllies.length===0) {
// 敗北
battleState.log.push("味方は全滅した…");
doLoseBattle();
return true;
}
// 敵生存
const aliveEnemies= battleState.combatants.filter(c => c.unitType==="enemy" && !c.isDown);
if(aliveEnemies.length===0) {
// 勝利
battleState.log.push("敵を全て倒した! 勝利!");
doWinBattle();
return true;
}
return false;
}
function doWinBattle() {
battleState.battleOver=true;
// ボス or 雑魚敵全体のEXP/Gold
let totalExp=0, totalGold=0;
battleState.combatants.forEach(c=>{
if(c.unitType==="enemy") {
if(c.rewardExp) totalExp += c.rewardExp; else totalExp += (c.exp||0);
if(c.rewardGold) totalGold += c.rewardGold; else totalGold += (c.gold||0);
}
});
// 盗賊スキルでGoldボーナス
if(gameData.hero.job==="盗賊"){
const tSkills=gameData.skills.thief;
tSkills.forEach(s=>{
if(s.name==="ゴールド盗み") {
totalGold+=(s.level*5);
}
});
}
gainExp(totalExp);
gainGold(totalGold);
battleState.log.push(`報酬 → EXP:${totalExp}, Gold:${totalGold}`);
// 倒れた味方をHP1で復帰
battleState.combatants.forEach(c=>{
if((c.unitType==="hero"||c.unitType==="party") && c.isDown){
c.isDown=false; c.hp=1;
if(c.ref) c.ref.hp=1;
} else {
if(c.ref) c.ref.hp=c.hp; // HPを反映
}
});
setTimeout(()=>{
endBattle(true);
},800);
}
function doLoseBattle() {
battleState.battleOver=true;
// ゴールド半減
gameData.hero.gold = Math.floor(gameData.hero.gold/2);
battleState.log.push("所持Goldが半分になった…");
// 全員HP1で復帰
battleState.combatants.forEach(c=>{
if(c.unitType==="hero"||c.unitType==="party"){
c.isDown=false;
c.hp=1;
if(c.ref) c.ref.hp=1;
}
});
setTimeout(()=>{
endBattle(false);
},800);
}
function endBattle(win) {
battleState.log.push(win ? "(勝利) バトル終了" : "(終了) バトル終了");
updateBattleUI();
setTimeout(()=>{
closeBattleModal();
},1000);
}
function calcPlayerTotalAttack(){
// ※ 旧の一斉攻撃計算用は使わなくなったが、参考に残しておく
return 10;
}
/* ボス撃破時の実績 & ストーリー */
function onBossDefeated(bossName) {
gameData.achievements.forEach(a => {
if(a.type==="bossKill" && a.bossName===bossName && !a.unlocked){
unlockAchievement(a);
}
});
const st= gameData.story.find(x=>x.bossName===bossName);
if(st){
battleState.log.push(st.text);
}
saveData();
}
/* =========================================
11) 実績 & ストーリー
========================================= */
function updateAchievementsUI() {
const listEl = document.getElementById("achievementList");
listEl.innerHTML = "";
gameData.achievements.forEach(a => {
const div = document.createElement("div");
div.className = "achievement";
if(!a.unlocked) div.classList.add("locked");
const h4 = document.createElement("h4");
h4.textContent = a.title;
const p = document.createElement("p");
p.textContent = a.desc;
if(!a.unlocked){
const lockedLabel=document.createElement("div");
lockedLabel.className="locked-label";
lockedLabel.textContent="Locked";
div.appendChild(lockedLabel);
}
div.appendChild(h4);
div.appendChild(p);
listEl.appendChild(div);
});
}
function checkAchievements() {
const hero=gameData.hero;
const totalQuestCompleted= gameData.dailyQuests.filter(q=>q.completed).length
+ gameData.normalQuests.filter(q=>q.completed).length
+ gameData.weeklyQuests.filter(q=>q.completed).length;
gameData.achievements.forEach(a=>{
if(a.unlocked) return;
switch(a.type){
case "questCount":
if(totalQuestCompleted>=a.target){
unlockAchievement(a);
}
break;
case "level":
if(hero.level>=a.target){
unlockAchievement(a);
}
break;
case "bossKill":
// boss討伐時に individually check
break;
case "gold":
if(hero.gold>=a.target){
unlockAchievement(a);
}
break;
}
});
}
function unlockAchievement(a) {
a.unlocked=true;
alert(`実績解除!「${a.title}」`);
saveData();
updateAchievementsUI();
}
function updateStoryProgress(){
let text="";
const bossKills= gameData.achievements.filter(a=>a.type==="bossKill"&&a.unlocked).map(a=>a.bossName);
bossKills.forEach(bn=>{
const st=gameData.story.find(x=>x.bossName===bn);
if(st){
text+=`<p>【${bn}】<br>${st.text}</p>`;
}
});
if(!text) text="<p>まだ大きなストーリーは進んでいません。</p>";
document.getElementById("storyProgress").innerHTML=text;
}
/* =========================================
12) UIの一括更新
========================================= */
function updateAllUI(){
updateHeroUI();
updatePartyUI();
updateSkillTreeUI();
updateCraftUI();
updateQuestsUI();
updateMapUI();
updateAchievementsUI();
updateStoryProgress();
}
</script>
</body>
</html>
サブスク自動整理・管理サービス
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>SUBS Management - サブスク自動整理・管理サービス</title>
<meta name="description" content="複数の定額サービスを一括管理・整理し、解約し忘れを防止して家計を賢くするプラットフォーム">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- ==========================
外部ライブラリ (CDN)
========================== -->
<!-- GSAP + ScrollTrigger -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"
integrity="sha512-xxxx"
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"
integrity="sha512-xxxx"
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<!-- Three.js -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r152/three.min.js"
integrity="sha512-xxxx"
crossorigin="anonymous"
referrerpolicy="no-referrer">
</script>
<style>
/* ==========================
ベーススタイル
========================== */
* {
box-sizing: border-box;
}
html, body {
margin: 0;
padding: 0;
font-family: sans-serif;
color: #333;
background-color: #f9f9f9;
line-height: 1.6;
scroll-behavior: smooth; /* スムーズスクロール */
}
a {
text-decoration: none;
color: #0078d7;
transition: color 0.2s ease;
}
a:hover {
color: #005bb5;
}
h1, h2, h3, h4 {
margin: 0;
font-weight: normal;
}
p {
margin: 0 0 1em;
}
ul {
margin: 0;
padding: 0;
list-style: none;
}
img {
max-width: 100%;
display: block;
}
/* ==========================
カラー変数
========================== */
:root {
--color-primary: #0078d7;
--color-accent: #ffcc00;
--color-bg: #f9f9f9;
--color-white: #ffffff;
--color-border: #eee;
--color-text: #333;
}
/* ==========================
全体のレイアウト用コンテナ
========================== */
.container {
width: 90%;
max-width: 1200px;
margin: 0 auto;
padding: 60px 0;
}
/* ==========================
ヘッダー
========================== */
header {
position: sticky;
top: 0;
z-index: 998;
background: var(--color-white);
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header-inner {
display: flex;
align-items: center;
justify-content: space-between;
height: 60px;
width: 90%;
max-width: 1200px;
margin: 0 auto;
}
.header-logo {
font-size: 1.5rem;
color: var(--color-primary);
font-weight: bold;
}
nav ul {
display: flex;
gap: 30px;
}
nav a {
color: var(--color-text);
font-size: 0.95rem;
font-weight: bold;
}
nav a:hover {
color: var(--color-primary);
}
.hamburger {
display: none;
font-size: 1.5rem;
cursor: pointer;
}
.mobile-nav {
display: none;
position: absolute;
top: 60px; right: 0;
width: 100%;
max-width: 300px;
background: var(--color-white);
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
z-index: 999;
}
.mobile-nav ul {
display: flex;
flex-direction: column;
gap: 20px;
padding: 20px;
}
.mobile-nav a {
font-size: 1rem;
color: var(--color-text);
}
/* ==========================
Three.js 背景キャンバス
========================== */
#three-bg {
position: fixed;
top: 0; left: 0;
width: 100vw; height: 100vh;
z-index: -1;
background: #000; /* 読み込み時の仮色 */
}
/* ==========================
ヒーローセクション
========================== */
.hero {
position: relative;
min-height: 80vh;
display: flex;
align-items: center;
justify-content: center;
text-align: center;
color: #fff;
padding: 100px 20px;
overflow: hidden;
}
.hero h1 {
font-size: 2.5rem;
margin-bottom: 20px;
line-height: 1.2;
transform: translateY(50px);
opacity: 0;
}
.hero p {
font-size: 1.1rem;
margin-bottom: 30px;
transform: translateY(50px);
opacity: 0;
}
.hero .cta-btn {
background: var(--color-accent);
color: #333;
padding: 15px 30px;
border-radius: 6px;
font-size: 1rem;
font-weight: bold;
transition: background-color 0.2s ease;
transform: translateY(50px);
opacity: 0;
}
.hero .cta-btn:hover {
background: #e6b800;
}
/* ==========================
Statsセクション (数値カウンター)
========================== */
.stats-section {
background: var(--color-bg);
text-align: center;
padding: 60px 20px;
}
.stats-section h2 {
color: var(--color-primary);
margin-bottom: 40px;
}
.stats-grid {
display: flex;
flex-wrap: wrap;
gap: 40px;
justify-content: center;
}
.stat-item {
flex: 1 1 200px;
max-width: 250px;
background: var(--color-white);
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
padding: 20px;
transition: transform 0.3s ease;
}
.stat-item:hover { transform: translateY(-5px); }
.stat-number {
font-size: 2rem;
color: var(--color-primary);
font-weight: bold;
margin-bottom: 10px;
}
.stat-label {
font-size: 0.95rem;
color: #666;
}
/* ==========================
Testimonials (カルーセル)
========================== */
.testimonials-section {
background: var(--color-white);
padding: 60px 20px;
}
.testimonials-section h2 {
text-align: center;
color: var(--color-primary);
margin-bottom: 40px;
}
.carousel-container {
max-width: 800px;
margin: 0 auto;
position: relative;
overflow: hidden;
}
.carousel-track {
display: flex;
transition: transform 0.5s ease;
}
.carousel-slide {
min-width: 100%;
background: var(--color-bg);
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
margin: 0 10px;
padding: 30px;
text-align: center;
}
.carousel-text {
font-size: 1rem;
font-style: italic;
color: #555;
margin-bottom: 10px;
}
.carousel-author {
font-weight: bold;
color: #333;
font-size: 0.9rem;
}
.carousel-buttons {
text-align: center;
margin-top: 20px;
}
.carousel-btn {
background: var(--color-primary);
color: #fff;
border: none;
padding: 8px 12px;
border-radius: 4px;
margin: 0 5px;
cursor: pointer;
font-size: 1rem;
}
.carousel-btn:hover {
background: #005bb5;
}
/* ==========================
フォームセクション (リアルタイムバリデーション)
========================== */
.form-section {
background: var(--color-accent);
padding: 60px 20px;
}
.form-section h2 {
text-align: center;
color: var(--color-primary);
margin-bottom: 40px;
}
.signup-form {
max-width: 600px;
margin: 0 auto;
background: var(--color-white);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
padding: 30px;
}
.form-group {
margin-bottom: 20px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input {
width: 100%;
padding: 10px;
border-radius: 4px;
border: 1px solid #ccc;
font-size: 1rem;
}
.error-message {
font-size: 0.85rem;
color: red;
display: none;
}
.signup-submit {
background: var(--color-primary);
color: #fff;
border: none;
padding: 12px 24px;
border-radius: 4px;
font-size: 1rem;
cursor: pointer;
transition: background 0.2s ease;
}
.signup-submit:hover {
background: #005bb5;
}
/* ==========================
フッター
========================== */
footer {
background: #f0f0f0;
padding: 20px;
text-align: center;
margin-top: 60px;
}
footer .footer-note {
font-size: 0.9rem;
color: #666;
margin-top: 10px;
}
/* ==========================
レスポンシブ
========================== */
@media (max-width: 768px) {
.header-inner {
justify-content: space-between;
}
nav ul { display: none; }
.hamburger { display: block; }
.stats-grid { display: block; }
.stat-item { margin: 0 auto 20px; }
.carousel-slide { margin: 0 5px; }
}
</style>
</head>
<body>
<!-- Three.js 背景キャンバス -->
<canvas id="three-bg" aria-hidden="true"></canvas>
<!-- ヘッダー -->
<header>
<div class="header-inner">
<div class="header-logo">SUBS Management</div>
<nav>
<ul id="navLinks">
<li><a href="#hero">Home</a></li>
<li><a href="#stats">Stats</a></li>
<li><a href="#testimonials">Reviews</a></li>
<li><a href="#signup">SignUp</a></li>
</ul>
</nav>
<!-- ハンバーガーメニュー -->
<div class="hamburger" id="hamburger">☰</div>
</div>
<!-- モバイル用ナビ -->
<div class="mobile-nav" id="mobileNav">
<ul>
<li><a href="#hero">Home</a></li>
<li><a href="#stats">Stats</a></li>
<li><a href="#testimonials">Reviews</a></li>
<li><a href="#signup">SignUp</a></li>
</ul>
</div>
</header>
<!-- ヒーローセクション -->
<section class="hero" id="hero">
<h1>サブスクを一括管理して、賢く節約</h1>
<p>
使っていないサブスクを一元化・解約サポート。<br>
無駄な出費を削減し、あなたの家計をスリムにしませんか?
</p>
<a href="#signup" class="cta-btn">今すぐ始める</a>
</section>
<!-- Statsセクション (数値カウンター) -->
<section class="stats-section" id="stats">
<div class="container">
<h2>導入実績・成果</h2>
<div class="stats-grid">
<div class="stat-item">
<div class="stat-number" data-target="12000">0</div>
<div class="stat-label">累計ユーザー</div>
</div>
<div class="stat-item">
<div class="stat-number" data-target="9800">0</div>
<div class="stat-label">解約サポート実行</div>
</div>
<div class="stat-item">
<div class="stat-number" data-target="95">0</div>
<div class="stat-label">満足度(%)</div>
</div>
<div class="stat-item">
<div class="stat-number" data-target="1500000">0</div>
<div class="stat-label">総節約額(円)</div>
</div>
</div>
</div>
</section>
<!-- Testimonials (カルーセル) -->
<section class="testimonials-section" id="testimonials">
<h2>利用者の声</h2>
<div class="carousel-container">
<div class="carousel-track" id="carouselTrack">
<!-- Slide1 -->
<div class="carousel-slide">
<p class="carousel-text">
「いくつもの動画配信サービスを契約していたのですが、まとめて管理できるから凄く楽になりました!」
</p>
<p class="carousel-author">- 田中さん (30代 / 会社員)</p>
</div>
<!-- Slide2 -->
<div class="carousel-slide">
<p class="carousel-text">
「解約リマインドが来るので本当に助かります。<br>
先月だけで2,000円以上も節約できました!」
</p>
<p class="carousel-author">- 佐藤さん (40代 / 主婦)</p>
</div>
<!-- Slide3 -->
<div class="carousel-slide">
<p class="carousel-text">
「支払い情報が一元化されるのは最高。<br>
何よりUIがシンプルで使いやすいです!」
</p>
<p class="carousel-author">- 鈴木さん (20代 / フリーランス)</p>
</div>
</div>
<div class="carousel-buttons">
<button class="carousel-btn" id="prevSlide"><</button>
<button class="carousel-btn" id="nextSlide">></button>
</div>
</div>
</section>
<!-- フォームセクション (リアルタイムバリデーション) -->
<section class="form-section" id="signup">
<div class="container">
<h2>新規登録 (無料)</h2>
<div class="signup-form" id="signupForm">
<div class="form-group">
<label for="signupEmail">メールアドレス</label>
<input type="email" id="signupEmail" aria-required="true" placeholder="example@example.com">
<p class="error-message" id="emailError">正しいメールアドレスを入力してください</p>
</div>
<div class="form-group">
<label for="signupPassword">パスワード (8文字以上)</label>
<input type="password" id="signupPassword" aria-required="true" placeholder="8文字以上で入力">
<p class="error-message" id="passwordError">パスワードは8文字以上で入力してください</p>
</div>
<button class="signup-submit" id="signupSubmitBtn">登録</button>
</div>
</div>
</section>
<!-- フッター -->
<footer>
<p class="footer-note">
© 2025 SUBS Management. All rights reserved.
</p>
</footer>
<script>
/*************************************************************
* 1. Three.js 背景
*************************************************************/
let scene, camera, renderer;
const canvas = document.getElementById('three-bg');
let objects = [];
function initThreeBG() {
scene = new THREE.Scene();
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
camera.position.z = 250;
renderer = new THREE.WebGLRenderer({ canvas: canvas, alpha: true });
renderer.setSize(window.innerWidth, window.innerHeight);
// ライト
const ambient = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambient);
const pointLight = new THREE.PointLight(0xffffff, 0.8);
pointLight.position.set(50, 50, 50);
scene.add(pointLight);
// いくつかのオブジェクトをランダム配置
const geometryTypes = [
new THREE.TorusGeometry(10, 3, 16, 100),
new THREE.SphereGeometry(8, 32, 32),
new THREE.BoxGeometry(12, 12, 12)
];
for(let i=0; i<30; i++){
const geo = geometryTypes[Math.floor(Math.random() * geometryTypes.length)];
const material = new THREE.MeshStandardMaterial({
color: 0xffffff
});
// 色相をランダム化
material.color.setHSL(Math.random(), 0.5, 0.5);
const mesh = new THREE.Mesh(geo, material);
mesh.position.x = (Math.random() - 0.5) * 400;
mesh.position.y = (Math.random() - 0.5) * 400;
mesh.position.z = (Math.random() - 0.5) * 400;
mesh.rotation.x = Math.random() * Math.PI;
mesh.rotation.y = Math.random() * Math.PI;
scene.add(mesh);
objects.push(mesh);
}
}
function animateThreeBG() {
requestAnimationFrame(animateThreeBG);
objects.forEach(obj => {
obj.rotation.x += 0.001;
obj.rotation.y += 0.001;
});
camera.position.z += Math.sin(Date.now() * 0.0005) * 0.05; // カメラ揺らし
renderer.render(scene, camera);
}
/*************************************************************
* 2. ハンバーガーメニュー
*************************************************************/
const hamburger = document.getElementById('hamburger');
const mobileNav = document.getElementById('mobileNav');
let navOpen = false;
hamburger.addEventListener('click', () => {
navOpen = !navOpen;
mobileNav.style.display = navOpen ? 'block' : 'none';
});
/*************************************************************
* 3. GSAP + ScrollTrigger
* - Heroセクションのタイトルやボタンをアニメ
* - 他のセクションもスクロールでフェードイン
*************************************************************/
window.addEventListener('DOMContentLoaded', () => {
gsap.registerPlugin(ScrollTrigger);
// Heroアニメ
gsap.to('.hero h1', {
duration: 1, y: 0, opacity: 1, ease: 'power2.out'
});
gsap.to('.hero p', {
duration: 1, y: 0, opacity: 1, delay: 0.2, ease: 'power2.out'
});
gsap.to('.hero .cta-btn', {
duration: 1, y: 0, opacity: 1, delay: 0.4, ease: 'power2.out'
});
// Statsセクション
gsap.from('#stats .stats-grid', {
scrollTrigger: {
trigger: '#stats',
start: 'top 80%',
},
y: 50, opacity: 0, duration: 1, ease: 'power2.out'
});
// Testimonials
gsap.from('#testimonials .carousel-container', {
scrollTrigger: {
trigger: '#testimonials',
start: 'top 80%',
},
y: 50, opacity: 0, duration: 1, ease: 'power2.out'
});
// SignUpフォーム
gsap.from('#signupForm', {
scrollTrigger: {
trigger: '#signupForm',
start: 'top 80%',
},
y: 50, opacity: 0, duration: 1, ease: 'power2.out'
});
});
/*************************************************************
* 4. Stats カウンター (Intersection Observer)
*************************************************************/
const statNumbers = document.querySelectorAll('.stat-number');
const statsObserver = new IntersectionObserver((entries, obs) => {
entries.forEach(entry => {
if(entry.isIntersecting) {
const el = entry.target;
const targetVal = parseInt(el.getAttribute('data-target'), 10);
let currentVal = 0;
const increment = Math.ceil(targetVal / 100);
const timer = setInterval(() => {
currentVal += increment;
if(currentVal >= targetVal) {
currentVal = targetVal;
clearInterval(timer);
}
el.textContent = currentVal.toLocaleString();
}, 20);
obs.unobserve(el);
}
});
}, { threshold: 0.5 });
statNumbers.forEach(num => statsObserver.observe(num));
/*************************************************************
* 5. Testimonials カルーセル
*************************************************************/
const carouselTrack = document.getElementById('carouselTrack');
const prevSlideBtn = document.getElementById('prevSlide');
const nextSlideBtn = document.getElementById('nextSlide');
let currentSlideIndex = 0;
const slides = Array.from(document.querySelectorAll('.carousel-slide'));
function updateCarousel() {
carouselTrack.style.transform = `translateX(${-100 * currentSlideIndex}%)`;
}
prevSlideBtn.addEventListener('click', () => {
currentSlideIndex = (currentSlideIndex === 0) ? slides.length - 1 : currentSlideIndex - 1;
updateCarousel();
});
nextSlideBtn.addEventListener('click', () => {
currentSlideIndex = (currentSlideIndex === slides.length - 1) ? 0 : currentSlideIndex + 1;
updateCarousel();
});
/*************************************************************
* 6. リアルタイム・フォームバリデーション
*************************************************************/
const signupEmail = document.getElementById('signupEmail');
const signupPassword = document.getElementById('signupPassword');
const emailError = document.getElementById('emailError');
const passwordError = document.getElementById('passwordError');
const signupSubmitBtn = document.getElementById('signupSubmitBtn');
function validateEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
}
function validatePassword(pass) {
return pass.length >= 8;
}
signupEmail.addEventListener('input', () => {
if(!validateEmail(signupEmail.value.trim())) {
emailError.style.display = 'block';
} else {
emailError.style.display = 'none';
}
});
signupPassword.addEventListener('input', () => {
if(!validatePassword(signupPassword.value.trim())) {
passwordError.style.display = 'block';
} else {
passwordError.style.display = 'none';
}
});
signupSubmitBtn.addEventListener('click', () => {
const emailVal = signupEmail.value.trim();
const passVal = signupPassword.value.trim();
let hasError = false;
if(!validateEmail(emailVal)) {
emailError.style.display = 'block';
hasError = true;
}
if(!validatePassword(passVal)) {
passwordError.style.display = 'block';
hasError = true;
}
if(hasError) return;
alert('登録ありがとうございました!');
// フォームリセット例
signupEmail.value = '';
signupPassword.value = '';
emailError.style.display = 'none';
passwordError.style.display = 'none';
});
/*************************************************************
* 7. 画面リサイズ時のThree.js再設定
*************************************************************/
window.addEventListener('resize', () => {
renderer.setSize(window.innerWidth, window.innerHeight);
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
});
/*************************************************************
* 8. ページ読み込み完了時に Three.js 初期化 & アニメ開始
*************************************************************/
window.addEventListener('load', () => {
initThreeBG();
animateThreeBG();
});
</script>
</body>
</html>
Voogle.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Voogle</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.css">
<style>
/* 全体の基本設定 */
body {
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
margin: 0;
padding: 0;
background: #f7f7f7;
color: #333;
line-height: 1.6;
}
header {
background: #003d66;
color: #fff;
padding: 20px;
text-align: center;
}
header h1 {
margin: 0;
font-size: 2.5em;
}
nav {
margin-top: 10px;
}
nav a {
color: #fff;
margin: 0 15px;
text-decoration: none;
font-size: 1em;
}
/* レイアウトコンテナ */
.container {
display: flex;
max-width: 1200px;
margin: 20px auto;
padding: 0 20px;
}
/* サイドバー */
.sidebar {
flex: 0 0 250px;
background: #fff;
padding: 20px;
margin-right: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
height: fit-content;
}
.sidebar h2 {
font-size: 1.3em;
margin-top: 0;
border-bottom: 1px solid #eee;
padding-bottom: 5px;
}
.sidebar section {
margin-bottom: 20px;
}
.sidebar label {
display: block;
margin-bottom: 5px;
}
.sidebar input[type="date"],
.sidebar select {
width: 100%;
padding: 8px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.sidebar .checkbox-group label {
display: inline-block;
margin-right: 10px;
font-size: 0.9em;
}
.sidebar ul {
list-style: none;
padding-left: 0;
}
.sidebar li {
padding: 5px 0;
cursor: pointer;
color: #005fa3;
}
.sidebar li:hover {
text-decoration: underline;
}
/* メインコンテンツ */
.main-content {
flex: 1;
background: #fff;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
/* 検索フォーム */
.search-section {
position: relative;
margin-bottom: 20px;
}
.search-form {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.search-form input[type="text"] {
flex: 1;
padding: 12px;
font-size: 1.1em;
border: 1px solid #ccc;
border-radius: 4px;
outline: none;
}
.search-form button {
padding: 12px 20px;
font-size: 1.1em;
border: none;
border-radius: 4px;
cursor: pointer;
}
#searchButton {
background: #005fa3;
color: #fff;
}
#searchButton:hover {
background: #00407a;
}
/* オートサジェスト */
.suggestions {
list-style: none;
margin: 0;
padding: 0;
position: absolute;
width: calc(100% - 20px);
background: #fff;
border: 1px solid #ccc;
border-top: none;
z-index: 10;
max-height: 200px;
overflow-y: auto;
}
.suggestions li {
padding: 8px 10px;
cursor: pointer;
}
.suggestions li:hover {
background: #f0f8ff;
}
/* ローディングスピナー */
.spinner {
display: none;
margin: 20px auto;
width: 50px;
height: 50px;
border: 6px solid rgba(0,0,0,0.1);
border-top-color: #005fa3;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* 検索結果カード */
.results {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 20px;
}
.result-item {
background: #fafafa;
border: 1px solid #eee;
padding: 15px;
border-radius: 8px;
transition: box-shadow 0.2s;
}
.result-item:hover {
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
.result-title {
font-size: 1.2em;
color: #005fa3;
margin: 0 0 5px 0;
}
.result-title a {
text-decoration: none;
color: inherit;
}
.result-url {
font-size: 0.85em;
color: #888;
margin-bottom: 8px;
word-break: break-all;
}
.result-snippet {
font-size: 0.95em;
margin-bottom: 10px;
}
.result-meta {
font-size: 0.8em;
color: #666;
}
/* ページネーション */
.pagination {
text-align: center;
margin: 30px 0;
}
.pagination a {
margin: 0 5px;
text-decoration: none;
color: #005fa3;
padding: 8px 12px;
border: 1px solid #ccc;
border-radius: 4px;
cursor: pointer;
}
.pagination a.active {
background: #005fa3;
color: #fff;
border-color: #005fa3;
}
.pagination a:hover {
background: #f0f8ff;
}
/* 「さらに読み込む」ボタン */
.load-more {
display: block;
width: 200px;
margin: 20px auto;
padding: 12px;
text-align: center;
background: #005fa3;
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
}
.load-more:hover {
background: #00407a;
}
/* フッター */
footer {
text-align: center;
padding: 15px;
font-size: 0.8em;
color: #aaa;
background: #f7f7f7;
margin-top: 20px;
}
/* レスポンシブ対応 */
@media (max-width: 800px) {
.container {
flex-direction: column;
}
.sidebar {
margin-right: 0;
margin-bottom: 20px;
}
}
</style>
</head>
<body>
<header>
<h1>Voogle検索エンジン</h1>
<nav>
<a href="#">ホーム</a>
<a href="#">使い方</a>
<a href="#">お問い合わせ</a>
</nav>
</header>
<div class="container">
<!-- サイドバー:フィルター・ソート・検索履歴 -->
<aside class="sidebar">
<section>
<h2>フィルター</h2>
<form id="filterForm">
<label for="startDate">開始日:</label>
<input type="date" id="startDate">
<label for="endDate">終了日:</label>
<input type="date" id="endDate">
<label>コンテンツタイプ:</label>
<div class="checkbox-group">
<!-- Wikipedia検索結果用として追加 -->
<label><input type="checkbox" name="contentType" value="wikipedia" checked> Wikipedia</label>
<!-- 他の項目は将来的な拡張用 -->
<label><input type="checkbox" name="contentType" value="news" checked> ニュース</label>
<label><input type="checkbox" name="contentType" value="blog" checked> ブログ</label>
<label><input type="checkbox" name="contentType" value="forum" checked> フォーラム</label>
</div>
<label for="sortOrder">ソート順:</label>
<select id="sortOrder">
<option value="relevance">関連順</option>
<option value="date">新着順</option>
</select>
<button type="submit" style="margin-top:10px; padding:8px 12px; background:#005fa3; color:#fff; border:none; border-radius:4px; cursor:pointer;">適用</button>
</form>
</section>
<section>
<h2>検索履歴</h2>
<ul id="searchHistory">
<!-- 最近の検索キーワードを表示 -->
</ul>
</section>
</aside>
<!-- メインコンテンツ:検索フォーム&結果 -->
<main class="main-content">
<div class="search-section">
<form id="searchForm" class="search-form">
<input type="text" id="searchInput" placeholder="検索キーワードを入力" autocomplete="off">
<button type="submit" id="searchButton">検索</button>
</form>
<!-- オートサジェスト -->
<ul id="suggestions" class="suggestions"></ul>
</div>
<!-- ローディングスピナー -->
<div id="spinner" class="spinner"></div>
<!-- 検索結果カード -->
<div id="results" class="results">
<!-- 結果はJavaScriptで動的レンダリング -->
</div>
<!-- ページネーション -->
<div id="pagination" class="pagination"></div>
<!-- Load More ボタン(ページネーションの代替) -->
<button id="loadMore" class="load-more">さらに読み込む</button>
</main>
</div>
<footer>
© 2025 Voogle. All rights reserved.
</footer>
<script>
// --- オートサジェスト用のサンプル候補 ---
const suggestionsData = [
"JavaScript", "HTML", "CSS", "クローラー", "検索エンジン", "Web開発", "React", "Node.js", "Python", "データスクレイピング"
];
const searchInput = document.getElementById('searchInput');
const suggestionsList = document.getElementById('suggestions');
searchInput.addEventListener('input', function() {
const query = this.value.toLowerCase();
suggestionsList.innerHTML = '';
if(query.length === 0) return;
const filtered = suggestionsData.filter(item => item.toLowerCase().includes(query));
filtered.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
li.addEventListener('click', () => {
searchInput.value = item;
suggestionsList.innerHTML = '';
});
suggestionsList.appendChild(li);
});
});
// --- 検索履歴管理 ---
const searchHistoryEl = document.getElementById('searchHistory');
function updateSearchHistory(query) {
let history = JSON.parse(localStorage.getItem('searchHistory')) || [];
if (!history.includes(query)) {
history.unshift(query);
if(history.length > 5) history.pop();
localStorage.setItem('searchHistory', JSON.stringify(history));
renderSearchHistory();
}
}
function renderSearchHistory() {
let history = JSON.parse(localStorage.getItem('searchHistory')) || [];
searchHistoryEl.innerHTML = '';
history.forEach(item => {
const li = document.createElement('li');
li.textContent = item;
li.addEventListener('click', () => {
searchInput.value = item;
performSearch(item);
});
searchHistoryEl.appendChild(li);
});
}
renderSearchHistory();
// --- Wikipedia検索結果のレンダリング ---
function renderResults(page, results) {
const resultsContainer = document.getElementById('results');
// 結果カードはすべて再レンダリング(Load Moreの場合は追記可能に変更も検討)
resultsContainer.innerHTML = '';
const startIndex = (page - 1) * resultsPerPage;
const endIndex = startIndex + resultsPerPage;
const pageResults = results.slice(startIndex, endIndex);
if(pageResults.length === 0) {
resultsContainer.innerHTML = '<p>該当する検索結果がありません。</p>';
return;
}
pageResults.forEach(result => {
const div = document.createElement('div');
div.className = 'result-item';
div.innerHTML = `
<h2 class="result-title"><a href="${result.url}" target="_blank">${result.title}</a></h2>
<div class="result-url">${result.url}</div>
<p class="result-snippet">${result.snippet}</p>
<div class="result-meta">投稿日: ${result.date} | タイプ: ${result.contentType}</div>
`;
resultsContainer.appendChild(div);
});
}
// --- ページネーションのレンダリング ---
function renderPagination(page, totalResults) {
const paginationContainer = document.getElementById('pagination');
paginationContainer.innerHTML = '';
const totalPages = Math.ceil(totalResults / resultsPerPage);
// 前へ
const prev = document.createElement('a');
prev.textContent = '«';
prev.addEventListener('click', () => {
if(page > 1) {
currentPage--;
renderResults(currentPage, filteredResults);
renderPagination(currentPage, filteredResults.length);
}
});
paginationContainer.appendChild(prev);
// ページ番号
for(let i = 1; i <= totalPages; i++) {
const pageLink = document.createElement('a');
pageLink.textContent = i;
if(i === page) pageLink.classList.add('active');
pageLink.addEventListener('click', () => {
currentPage = i;
renderResults(currentPage, filteredResults);
renderPagination(currentPage, filteredResults.length);
});
paginationContainer.appendChild(pageLink);
}
// 次へ
const next = document.createElement('a');
next.textContent = '»';
next.addEventListener('click', () => {
if(page < totalPages) {
currentPage++;
renderResults(currentPage, filteredResults);
renderPagination(currentPage, filteredResults.length);
}
});
paginationContainer.appendChild(next);
}
// --- 定数・変数 ---
let filteredResults = []; // Wikipedia APIから取得した結果を格納
let currentPage = 1;
const resultsPerPage = 10;
const spinner = document.getElementById('spinner');
// --- Wikipedia APIを利用した検索実行 ---
const searchForm = document.getElementById('searchForm');
searchForm.addEventListener('submit', function(e) {
e.preventDefault();
const query = searchInput.value.trim();
performSearch(query);
});
function performSearch(query) {
updateSearchHistory(query);
spinner.style.display = 'block';
// Wikipedia APIエンドポイント(origin=* を指定してクロスオリジン対応)
const url = "https://ja.wikipedia.org/w/api.php?action=query&list=search&format=json&origin=*&srsearch=" + encodeURIComponent(query) + "&srlimit=20";
fetch(url)
.then(response => response.json())
.then(data => {
spinner.style.display = 'none';
if (data.query && data.query.search && data.query.search.length > 0) {
// APIから取得した各項目のtimestampを日付文字列に変換
filteredResults = data.query.search.map(item => {
return {
title: item.title,
snippet: item.snippet,
url: "https://ja.wikipedia.org/?curid=" + item.pageid,
date: new Date(item.timestamp).toISOString().split('T')[0],
contentType: "wikipedia"
};
});
currentPage = 1;
renderResults(currentPage, filteredResults);
renderPagination(currentPage, filteredResults.length);
} else {
document.getElementById('results').innerHTML = '<p>該当する検索結果がありません。</p>';
}
})
.catch(error => {
spinner.style.display = 'none';
console.error('Error:', error);
document.getElementById('results').innerHTML = '<p>検索結果の取得に失敗しました。</p>';
});
}
// --- サイドバーのフィルター処理 ---
const filterForm = document.getElementById('filterForm');
filterForm.addEventListener('submit', function(e) {
e.preventDefault();
applySidebarFilters();
currentPage = 1;
renderResults(currentPage, filteredResults);
renderPagination(currentPage, filteredResults.length);
});
function applySidebarFilters() {
// フィルター設定の取得
const startDate = document.getElementById('startDate').value;
const endDate = document.getElementById('endDate').value;
const sortOrder = document.getElementById('sortOrder').value;
const checkboxes = document.querySelectorAll('input[name="contentType"]:checked');
const types = Array.from(checkboxes).map(cb => cb.value);
// 現在のfilteredResultsに対してさらにフィルタリング
filteredResults = filteredResults.filter(result => {
let valid = true;
if(startDate && result.date < startDate) valid = false;
if(endDate && result.date > endDate) valid = false;
if(types.length && !types.includes(result.contentType)) valid = false;
return valid;
});
// ソート処理:日付の降順(新着順)の場合
if(sortOrder === "date") {
filteredResults.sort((a,b) => new Date(b.date) - new Date(a.date));
}
}
// --- Load More ボタン ---
const loadMoreBtn = document.getElementById('loadMore');
loadMoreBtn.addEventListener('click', () => {
const totalPages = Math.ceil(filteredResults.length / resultsPerPage);
if(currentPage < totalPages) {
currentPage++;
// 既存の結果に追記する方式
const resultsContainer = document.getElementById('results');
const startIndex = (currentPage - 1) * resultsPerPage;
const endIndex = startIndex + resultsPerPage;
const pageResults = filteredResults.slice(startIndex, endIndex);
pageResults.forEach(result => {
const div = document.createElement('div');
div.className = 'result-item';
div.innerHTML = `
<h2 class="result-title"><a href="${result.url}" target="_blank">${result.title}</a></h2>
<div class="result-url">${result.url}</div>
<p class="result-snippet">${result.snippet}</p>
<div class="result-meta">投稿日: ${result.date} | タイプ: ${result.contentType}</div>
`;
resultsContainer.appendChild(div);
});
renderPagination(currentPage, filteredResults.length);
}
});
</script>
</body>
</html>
minisns.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>スフィア</title>
<style>
/* ======= 基本リセット ======= */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: Arial, sans-serif; background: #f7f9fc; color: #333; line-height: 1.5; }
header { background: #4a90e2; color: #fff; padding: 15px; text-align: center; }
header h1 { font-size: 28px; }
main { width: 90%; max-width: 800px; margin: 20px auto; }
/* ======= ユーザーアイコン ======= */
.user-icon { width: 40px; height: 40px; border-radius: 50%; margin-right: 10px; vertical-align: middle; }
/* ======= 認証セクション ======= */
.auth-section { background: #fff; padding: 15px; border-radius: 8px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.auth-section h2 { margin-bottom: 10px; }
.auth-section input { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid #ccc; border-radius: 4px; }
.auth-section button { background: #4a90e2; color: #fff; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; }
.auth-section button:hover { background: #357ab8; }
.toggle-auth { margin-top: 10px; font-size: 14px; color: #4a90e2; cursor: pointer; text-decoration: underline; }
/* ======= 投稿フォーム ======= */
.post-form { background: #fff; padding: 15px; margin-bottom: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.post-form textarea { width: 100%; padding: 10px; border: 1px solid #ccc; border-radius: 4px; resize: vertical; font-size: 14px; }
.post-form button { background: #4a90e2; color: #fff; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; margin-top: 10px; }
.post-form button:hover { background: #357ab8; }
/* ======= 検索セクション ======= */
.search-section { background: #fff; padding: 10px; margin-bottom: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.search-section input { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; font-size: 14px; }
/* ======= タイムライン ======= */
.timeline { display: flex; flex-direction: column; gap: 15px; }
.post-item { background: #fff; padding: 15px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); position: relative; }
.post-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
.post-username { font-weight: bold; }
.post-meta { font-size: 12px; color: #777; }
.post-content { font-size: 16px; margin-bottom: 10px; }
.post-actions { margin-top: 10px; }
.post-actions button { background: transparent; border: none; color: #4a90e2; cursor: pointer; margin-right: 10px; font-size: 14px; }
.post-actions button:hover { text-decoration: underline; }
.replies { margin-top: 15px; border-top: 1px solid #eee; padding-top: 10px; }
.reply-item { background: #f2f2f2; padding: 10px; border-radius: 6px; margin-bottom: 10px; }
.reply-header { font-size: 13px; font-weight: bold; margin-bottom: 5px; }
.reply-meta { font-size: 11px; color: #555; text-align: right; }
.edit-area { margin-top: 10px; }
.edit-area textarea { width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; }
.edit-area button { background: #4a90e2; color: #fff; border: none; padding: 6px 10px; border-radius: 4px; cursor: pointer; margin-top: 5px; }
.edit-area button:hover { background: #357ab8; }
/* ======= ユーティリティ(ログ/RSS)セクション ======= */
.utility-section { margin-top: 20px; text-align: center; }
.logs-section { background: #fff; padding: 15px; margin-top: 10px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-size: 12px; color: #555; max-height: 200px; overflow-y: auto; }
.log-entry { border-bottom: 1px solid #eee; padding: 5px 0; }
footer { text-align: center; padding: 15px; font-size: 14px; color: #777; margin-top: 20px; }
</style>
</head>
<body>
<header>
<h1>スフィア</h1>
</header>
<main>
<!-- 認証セクション(ログイン/登録 or ユーザー情報表示) -->
<section id="authSection" class="auth-section"></section>
<!-- 投稿フォーム(ログイン中のみ表示) -->
<section id="postFormSection" class="post-form" style="display: none;">
<textarea id="postContent" placeholder="いまどうしてる?"></textarea>
<button id="postButton">投稿する</button>
</section>
<!-- 検索セクション -->
<section class="search-section">
<input type="text" id="searchInput" placeholder="投稿を検索">
</section>
<!-- タイムライン -->
<section id="timeline" class="timeline"></section>
<!-- ユーティリティ:ログ表示/RSSダウンロード -->
<section class="utility-section">
<button id="toggleLogsButton">ログを表示</button>
<button id="downloadRSSButton">RSSフィードをダウンロード</button>
</section>
<!-- ログ表示セクション(初期は非表示) -->
<section id="logsSection" class="logs-section" style="display: none;"></section>
</main>
<footer>
<p>© 2023 スフィア</p>
</footer>
<script>
(function(){
'use strict';
// === 定数&キー定義 ===
const POSTS_KEY = 'posts';
const USERS_KEY = 'users';
const CURRENT_USER_KEY = 'currentUser';
const LOGS_KEY = 'logs';
// === データ管理変数 ===
let posts = [];
let users = [];
let currentUser = null;
let logs = [];
// === ストレージ関連関数 ===
const saveToStorage = (key, data) => localStorage.setItem(key, JSON.stringify(data));
const loadFromStorage = (key) => {
const stored = localStorage.getItem(key);
return stored ? JSON.parse(stored) : null;
};
const loadData = () => {
posts = loadFromStorage(POSTS_KEY) || [];
users = loadFromStorage(USERS_KEY) || [];
currentUser = loadFromStorage(CURRENT_USER_KEY) || null;
logs = loadFromStorage(LOGS_KEY) || [];
};
const saveData = () => {
saveToStorage(POSTS_KEY, posts);
saveToStorage(USERS_KEY, users);
saveToStorage(CURRENT_USER_KEY, currentUser);
saveToStorage(LOGS_KEY, logs);
};
// === ログ処理 ===
const addLog = (message) => {
const entry = { timestamp: new Date(), message };
logs.push(entry);
saveToStorage(LOGS_KEY, logs);
};
const renderLogs = () => {
const logsSection = document.getElementById('logsSection');
logsSection.innerHTML = '';
logs.forEach(log => {
const div = document.createElement('div');
div.className = 'log-entry';
div.textContent = `[${formatDate(log.timestamp)}] ${log.message}`;
logsSection.appendChild(div);
});
};
// === 日付フォーマット ===
const formatDate = (date) => {
const d = new Date(date);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
const h = String(d.getHours()).padStart(2, '0');
const min = String(d.getMinutes()).padStart(2, '0');
return `${y}-${m}-${day} ${h}:${min}`;
};
// === RSS生成 ===
const generateRSS = () => {
const now = new Date();
let rss = `<?xml version="1.0" encoding="UTF-8"?>\n`;
rss += `<rss version="2.0">\n<channel>\n`;
rss += `<title>Advanced Mini SNS RSS Feed</title>\n`;
rss += `<link>${location.origin + location.pathname}</link>\n`;
rss += `<description>RSS Feed of posts from Advanced Mini SNS</description>\n`;
rss += `<lastBuildDate>${now.toUTCString()}</lastBuildDate>\n`;
rss += `<language>ja</language>\n`;
posts.forEach(post => {
const postUrl = `${location.origin + location.pathname}?post=${post.id}`;
const title = `Post by ${post.username}: ${post.content.substring(0,20)}...`;
rss += `<item>\n`;
rss += `<title>${title}</title>\n`;
rss += `<link>${postUrl}</link>\n`;
rss += `<description><![CDATA[${post.content}]]></description>\n`;
rss += `<pubDate>${new Date(post.timestamp).toUTCString()}</pubDate>\n`;
rss += `<guid>${post.id}</guid>\n`;
rss += `</item>\n`;
});
rss += `</channel>\n</rss>`;
return rss;
};
const downloadRSS = () => {
const rssContent = generateRSS();
const blob = new Blob([rssContent], { type: 'application/rss+xml' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'feed.xml';
a.click();
URL.revokeObjectURL(url);
addLog('RSSフィードをダウンロード');
};
// === 認証処理 ===
const renderAuthSection = () => {
const authSection = document.getElementById('authSection');
authSection.innerHTML = '';
if (currentUser) {
// ログイン済み:アイコンとウェルカムメッセージ、ログアウトボタン
const container = document.createElement('div');
if (currentUser.icon) {
const img = document.createElement('img');
img.src = currentUser.icon;
img.className = 'user-icon';
container.appendChild(img);
}
const welcomeDiv = document.createElement('span');
welcomeDiv.textContent = `ようこそ、${currentUser.username} さん!`;
container.appendChild(welcomeDiv);
const logoutBtn = document.createElement('button');
logoutBtn.textContent = 'ログアウト';
logoutBtn.onclick = () => {
addLog(`ユーザー ${currentUser.username} ログアウト`);
currentUser = null;
saveData();
renderAuthSection();
togglePostForm();
renderTimeline();
};
authSection.appendChild(container);
authSection.appendChild(logoutBtn);
} else {
// 未ログイン:ログインフォームを表示
renderLoginForm(authSection);
}
};
const renderLoginForm = (container) => {
container.innerHTML = '<h2>ログイン</h2>';
const usernameInput = document.createElement('input');
usernameInput.type = 'text';
usernameInput.placeholder = 'ユーザー名';
const passwordInput = document.createElement('input');
passwordInput.type = 'password';
passwordInput.placeholder = 'パスワード';
const loginBtn = document.createElement('button');
loginBtn.textContent = 'ログイン';
loginBtn.onclick = () => {
const username = usernameInput.value.trim();
const password = passwordInput.value;
if (!username || !password) { alert('ユーザー名とパスワードを入力してください。'); return; }
const user = users.find(u => u.username === username && u.password === password);
if (user) {
currentUser = user;
saveData();
addLog(`ユーザー ${username} ログイン成功`);
renderAuthSection();
togglePostForm();
renderTimeline();
} else {
alert('ユーザー名またはパスワードが正しくありません。');
}
};
container.appendChild(usernameInput);
container.appendChild(passwordInput);
container.appendChild(loginBtn);
const toggleLink = document.createElement('div');
toggleLink.className = 'toggle-auth';
toggleLink.textContent = '新規登録はこちら';
toggleLink.onclick = () => renderRegisterForm(container);
container.appendChild(toggleLink);
};
const renderRegisterForm = (container) => {
container.innerHTML = '<h2>新規登録</h2>';
const usernameInput = document.createElement('input');
usernameInput.type = 'text';
usernameInput.placeholder = 'ユーザー名';
const passwordInput = document.createElement('input');
passwordInput.type = 'password';
passwordInput.placeholder = 'パスワード';
const confirmInput = document.createElement('input');
confirmInput.type = 'password';
confirmInput.placeholder = 'パスワード確認';
// アイコンアップロード用フィールド
const iconInput = document.createElement('input');
iconInput.type = 'file';
iconInput.accept = 'image/*';
iconInput.style.marginBottom = '10px';
const registerBtn = document.createElement('button');
registerBtn.textContent = '登録';
registerBtn.onclick = () => {
const username = usernameInput.value.trim();
const password = passwordInput.value;
const confirmPassword = confirmInput.value;
if (!username || !password || !confirmPassword) { alert('全ての項目を入力してください。'); return; }
if (password !== confirmPassword) { alert('パスワードが一致しません。'); return; }
if (users.some(u => u.username === username)) { alert('そのユーザー名は既に使用されています。'); return; }
// アイコンが選択されている場合、FileReader で Data URL に変換
if (iconInput.files && iconInput.files[0]) {
const reader = new FileReader();
reader.onload = function(e) {
const iconData = e.target.result;
const newUser = { username, password, icon: iconData };
users.push(newUser);
saveData();
addLog(`ユーザー ${username} 登録完了(アイコン設定済み)`);
alert('登録が完了しました。ログインしてください。');
renderLoginForm(container);
};
reader.readAsDataURL(iconInput.files[0]);
} else {
const newUser = { username, password, icon: '' };
users.push(newUser);
saveData();
addLog(`ユーザー ${username} 登録完了(アイコン未設定)`);
alert('登録が完了しました。ログインしてください。');
renderLoginForm(container);
}
};
container.appendChild(usernameInput);
container.appendChild(passwordInput);
container.appendChild(confirmInput);
container.appendChild(iconInput);
container.appendChild(registerBtn);
const toggleLink = document.createElement('div');
toggleLink.className = 'toggle-auth';
toggleLink.textContent = 'ログインはこちら';
toggleLink.onclick = () => renderLoginForm(container);
container.appendChild(toggleLink);
};
const togglePostForm = () => {
const postFormSection = document.getElementById('postFormSection');
postFormSection.style.display = currentUser ? 'block' : 'none';
};
// === タイムライン&投稿表示 ===
const renderTimeline = () => {
const timeline = document.getElementById('timeline');
timeline.innerHTML = '';
let filteredPosts = posts.slice();
const searchValue = document.getElementById('searchInput').value.trim().toLowerCase();
if (searchValue) {
filteredPosts = filteredPosts.filter(post =>
post.content.toLowerCase().includes(searchValue) ||
post.username.toLowerCase().includes(searchValue)
);
}
// 新しい投稿順にソート
filteredPosts.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));
filteredPosts.forEach(post => {
timeline.appendChild(renderPost(post));
});
};
const renderPost = (post) => {
const postDiv = document.createElement('div');
postDiv.className = 'post-item';
postDiv.setAttribute('data-id', post.id);
// ヘッダー(投稿者名&日時)
const headerDiv = document.createElement('div');
headerDiv.className = 'post-header';
const usernameSpan = document.createElement('span');
usernameSpan.className = 'post-username';
usernameSpan.textContent = post.username;
const metaSpan = document.createElement('span');
metaSpan.className = 'post-meta';
metaSpan.textContent = formatDate(post.timestamp);
headerDiv.appendChild(usernameSpan);
headerDiv.appendChild(metaSpan);
postDiv.appendChild(headerDiv);
// 投稿内容(編集モードか通常表示か)
const contentDiv = document.createElement('div');
contentDiv.className = 'post-content';
if (post.editing) {
const editArea = document.createElement('div');
editArea.className = 'edit-area';
const editTextarea = document.createElement('textarea');
editTextarea.value = post.content;
editArea.appendChild(editTextarea);
const saveBtn = document.createElement('button');
saveBtn.textContent = '保存';
saveBtn.onclick = () => {
const newContent = editTextarea.value.trim();
if (!newContent) { alert('投稿内容が空です。'); return; }
post.content = newContent;
post.editing = false;
saveData();
addLog(`投稿 ${post.id} 編集完了`);
renderTimeline();
};
const cancelBtn = document.createElement('button');
cancelBtn.textContent = 'キャンセル';
cancelBtn.style.marginLeft = '5px';
cancelBtn.onclick = () => { post.editing = false; renderTimeline(); };
editArea.appendChild(saveBtn);
editArea.appendChild(cancelBtn);
contentDiv.appendChild(editArea);
} else {
contentDiv.textContent = post.content;
}
postDiv.appendChild(contentDiv);
// アクションボタン(いいね、編集、削除、返信)
const actionsDiv = document.createElement('div');
actionsDiv.className = 'post-actions';
const likeBtn = document.createElement('button');
likeBtn.textContent = `いいね (${post.likes || 0})`;
likeBtn.onclick = () => {
post.likes = (post.likes || 0) + 1;
saveData();
addLog(`投稿 ${post.id} にいいね`);
renderTimeline();
};
actionsDiv.appendChild(likeBtn);
if (currentUser && currentUser.username === post.username) {
const editBtn = document.createElement('button');
editBtn.textContent = '編集';
editBtn.onclick = () => { post.editing = true; renderTimeline(); };
const deleteBtn = document.createElement('button');
deleteBtn.textContent = '削除';
deleteBtn.onclick = () => {
if (confirm('本当に削除しますか?')) {
posts = posts.filter(p => p.id !== post.id);
saveData();
addLog(`投稿 ${post.id} 削除`);
renderTimeline();
}
};
actionsDiv.appendChild(editBtn);
actionsDiv.appendChild(deleteBtn);
}
if (currentUser) {
const replyBtn = document.createElement('button');
replyBtn.textContent = '返信';
replyBtn.onclick = () => toggleReplyForm(post.id);
actionsDiv.appendChild(replyBtn);
}
postDiv.appendChild(actionsDiv);
// 返信一覧表示
const repliesDiv = document.createElement('div');
repliesDiv.className = 'replies';
if (post.replies && post.replies.length > 0) {
post.replies.forEach(reply => {
repliesDiv.appendChild(renderReply(reply));
});
}
postDiv.appendChild(repliesDiv);
// 返信フォーム(初期は非表示)
const replyFormDiv = document.createElement('div');
replyFormDiv.className = 'reply-form';
replyFormDiv.style.display = 'none';
const replyTextarea = document.createElement('textarea');
replyTextarea.placeholder = '返信内容を入力';
const submitReplyBtn = document.createElement('button');
submitReplyBtn.textContent = '返信する';
submitReplyBtn.onclick = () => {
const replyContent = replyTextarea.value.trim();
if (!replyContent) { alert('返信内容が空です。'); return; }
addReply(post.id, replyContent);
replyTextarea.value = '';
toggleReplyForm(post.id, true);
};
replyFormDiv.appendChild(replyTextarea);
replyFormDiv.appendChild(submitReplyBtn);
postDiv.appendChild(replyFormDiv);
return postDiv;
};
const renderReply = (reply) => {
const replyDiv = document.createElement('div');
replyDiv.className = 'reply-item';
const replyHeader = document.createElement('div');
replyHeader.className = 'reply-header';
replyHeader.textContent = reply.username;
const replyContent = document.createElement('div');
replyContent.textContent = reply.content;
const replyMeta = document.createElement('div');
replyMeta.className = 'reply-meta';
replyMeta.textContent = formatDate(reply.timestamp);
replyDiv.appendChild(replyHeader);
replyDiv.appendChild(replyContent);
replyDiv.appendChild(replyMeta);
return replyDiv;
};
const toggleReplyForm = (postId, forceHide = false) => {
const postDiv = document.querySelector(`.post-item[data-id="${postId}"]`);
if (!postDiv) return;
const replyForm = postDiv.querySelector('.reply-form');
replyForm.style.display = (forceHide || replyForm.style.display === 'block') ? 'none' : 'block';
};
const addReply = (postId, replyContent) => {
const post = posts.find(p => p.id === postId);
if (!post) return;
if (!post.replies) { post.replies = []; }
post.replies.push({
id: Date.now(),
username: currentUser.username,
content: replyContent,
timestamp: new Date()
});
saveData();
addLog(`投稿 ${post.id} に返信追加`);
renderTimeline();
};
// === 投稿作成処理 ===
const initPostButton = () => {
document.getElementById('postButton').addEventListener('click', () => {
const postContentElem = document.getElementById('postContent');
const content = postContentElem.value.trim();
if (!content) { alert('投稿内容が空です。'); return; }
const newPost = {
id: Date.now(),
username: currentUser.username,
content: content,
timestamp: new Date(),
likes: 0,
replies: []
};
posts.push(newPost);
saveData();
addLog(`投稿 ${newPost.id} 追加`);
renderTimeline();
postContentElem.value = '';
});
};
// === ユーティリティ処理:ログ/RSS ===
const initUtilityButtons = () => {
document.getElementById('toggleLogsButton').addEventListener('click', () => {
const logsSection = document.getElementById('logsSection');
if (logsSection.style.display === 'none' || logsSection.style.display === '') {
renderLogs();
logsSection.style.display = 'block';
document.getElementById('toggleLogsButton').textContent = 'ログを非表示';
} else {
logsSection.style.display = 'none';
document.getElementById('toggleLogsButton').textContent = 'ログを表示';
}
});
document.getElementById('downloadRSSButton').addEventListener('click', downloadRSS);
};
// === 初期化処理 ===
const init = () => {
loadData();
renderAuthSection();
togglePostForm();
renderTimeline();
initPostButton();
initUtilityButtons();
document.getElementById('searchInput').addEventListener('input', renderTimeline);
};
document.addEventListener('DOMContentLoaded', init);
})();
</script>
</body>
</html>
リアルタイムRSSトレンド.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>リアルタイムRSSトレンド</title>
<style>
body { font-family: Arial, sans-serif; background-color: #f4f4f4; margin: 0; padding: 0; }
h1 { text-align: center; padding: 20px; background: #333; color: white; }
#rss-feed { max-width: 800px; margin: 20px auto; padding: 20px; background: white; box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); }
.article { border-bottom: 1px solid #ccc; padding: 15px; display: flex; flex-direction: column; }
.article:last-child { border-bottom: none; }
.article a { text-decoration: none; color: #333; font-weight: bold; font-size: 16px; }
.article a:hover { color: #007bff; }
.article p { margin: 5px 0; color: #666; font-size: 14px; }
.loader { text-align: center; font-size: 18px; color: #666; }
.refresh-btn { display: block; width: 200px; margin: 20px auto; padding: 10px; background: #007bff; color: white; text-align: center; cursor: pointer; border-radius: 5px; }
.refresh-btn:hover { background: #0056b3; }
.error { text-align: center; color: red; font-weight: bold; }
</style>
</head>
<body>
<h1>リアルタイムRSSトレンド</h1>
<div id="rss-feed" class="loader">読み込み中...</div>
<div class="refresh-btn" onclick="loadRSS()">最新の情報を取得</div>
<div id="error-message" class="error"></div>
<script>
async function fetchRSS(url) {
try {
const response = await fetch(`https://api.rss2json.com/v1/api.json?rss_url=${encodeURIComponent(url)}`);
if (!response.ok) throw new Error("RSS取得に失敗しました");
const data = await response.json();
return data.items || [];
} catch (error) {
console.error("RSSの取得に失敗しました", error);
return [];
}
}
async function loadRSS() {
const feedElement = document.getElementById("rss-feed");
const errorElement = document.getElementById("error-message");
feedElement.innerHTML = "読み込み中...";
errorElement.innerHTML = "";
const rssUrls = [
"https://news.google.com/rss?hl=ja&gl=JP&ceid=JP:ja",
"https://rss.itmedia.co.jp/rss/2.0/news_bursts.xml",
"https://www3.nhk.or.jp/rss/news/cat0.xml"
];
let allArticles = [];
for (const url of rssUrls) {
const articles = await fetchRSS(url);
allArticles = allArticles.concat(articles);
}
if (allArticles.length === 0) {
feedElement.innerHTML = "";
errorElement.innerHTML = "記事が取得できませんでした。";
return;
}
allArticles.sort((a, b) => new Date(b.pubDate) - new Date(a.pubDate));
feedElement.innerHTML = "";
allArticles.forEach(article => {
const articleElement = document.createElement("div");
articleElement.classList.add("article");
articleElement.innerHTML = `
<a href="${article.link}" target="_blank">${article.title}</a>
<p>公開日時: ${new Date(article.pubDate).toLocaleString()}</p>
`;
feedElement.appendChild(articleElement);
});
}
document.addEventListener("DOMContentLoaded", loadRSS);
setInterval(loadRSS, 300000); // 5分ごとに自動更新
</script>
</body>
</html>
VRRPG.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>VRRPG - 拡張版 AR/VR RPG</title>
<!-- A-Frame ライブラリ -->
<script src="https://aframe.io/releases/1.4.0/aframe.min.js"></script>
<!-- Particle system コンポーネント(パーティクル演出用) -->
<script src="https://cdn.jsdelivr.net/gh/IdeaSpaceVR/aframe-particle-system-component@master/dist/aframe-particle-system-component.min.js"></script>
<style>
body { margin: 0; overflow: hidden; }
/* 各種オーバーレイ */
#mainMenuOverlay, #upgradeOverlay, #pauseOverlay, #gameOverOverlay {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
background: rgba(0,0,0,0.8);
color: #FFF;
display: flex;
justify-content: center;
align-items: center;
font-size: 48px;
z-index: 999;
display: none;
text-align: center;
flex-direction: column;
}
#mainMenuOverlay button, #upgradeOverlay button, #gameOverOverlay button {
font-size: 36px;
padding: 20px 40px;
margin-top: 20px;
}
</style>
</head>
<body>
<!-- メインメニュー -->
<div id="mainMenuOverlay" style="display: flex;">
<div>
<div>VRRPG - 拡張版 AR/VR RPG</div>
<button id="startButton">Start Game</button>
</div>
</div>
<!-- アップグレードストア -->
<div id="upgradeOverlay">
<div>
<div>Upgrade Store</div>
<div>Press 1: Increase Sword Damage (+10) (Cost: 50 Score)</div>
<div>Press 2: Increase Max Health (+20) (Cost: 50 Score)</div>
<button id="closeUpgrade">Close</button>
</div>
</div>
<!-- ポーズオーバーレイ -->
<div id="pauseOverlay">Paused</div>
<!-- ゲームオーバーオーバーレイ -->
<div id="gameOverOverlay">
<div>
<div>Game Over!</div>
<div id="finalScore">Final Score: 0</div>
<button id="restartButton">Restart</button>
</div>
</div>
<!-- AR/VRシーン:XRモードを AR に設定(Oculusパススルー利用) -->
<a-scene xr="mode: ar; referenceSpaceType: local-floor">
<!-- 背景音楽 -->
<a-entity id="bg-music" sound="src: url(bg-music.mp3); autoplay: true; loop: true; volume: 0.3"></a-entity>
<!-- 環境 -->
<a-sky color="#88ccee"></a-sky>
<a-plane position="0 0 0" rotation="-90 0 0" width="30" height="30" color="#77aa55"></a-plane>
<a-light type="directional" intensity="0.8" position="0 10 5"></a-light>
<!-- プレイヤー(カメラ、HUD、ポーズ対応) -->
<a-entity id="player" weapon-switcher position="0 1.6 5">
<a-camera wasd-controls look-controls>
<!-- 右手:カメラ内右下に固定表示(装備品) -->
<a-entity id="rightHand" position="0.5 -0.3 -1"></a-entity>
</a-camera>
<!-- HUD -->
<a-entity id="hud" position="0 -0.5 -1.5">
<a-text id="scoreText" value="Score: 0" position="-1 0.7 0" color="#FFF" width="4"></a-text>
<a-text id="healthText" value="Health: 100" position="-1 0.4 0" color="#FFF" width="4"></a-text>
<a-text id="waveText" value="Wave: 1" position="-1 0.1 0" color="#FFF" width="4"></a-text>
<a-text id="levelText" value="Lv: 1 Exp: 0" position="-1 -0.2 0" color="#FFF" width="4"></a-text>
<a-text id="weaponText" value="Weapon: None" position="-1 -0.5 0" color="#FFF" width="4"></a-text>
</a-entity>
</a-entity>
<!-- 落ちている剣(Sword) -->
<a-entity id="sword" position="0.3 1 -2" sword-swing pickup>
<!-- ブレード -->
<a-entity geometry="primitive: box; height: 1; width: 0.1; depth: 0.05"
material="color: silver; metalness: 0.8; roughness: 0.2"
position="0 0.5 0"></a-entity>
<!-- ガード -->
<a-entity geometry="primitive: box; height: 0.2; width: 0.3; depth: 0.05"
material="color: gold"
position="0 0.05 0"></a-entity>
<!-- ハンドル -->
<a-entity geometry="primitive: cylinder; radius: 0.05; height: 0.4"
material="color: brown"
position="0 -0.3 0" rotation="90 0 0"></a-entity>
<!-- 回転アニメーション(拾われるまで) -->
<a-animation attribute="rotation" dur="3000" fill="forwards" to="0 360 0" repeat="indefinite"></a-animation>
</a-entity>
<!-- 魔法の杖(Magic Wand) ※ 未使用 -->
<a-entity id="magicWand" geometry="primitive: cylinder; height: 0.8; radius: 0.05"
material="color: purple; emissive: #aa00ff"
position="0.3 1 -0.5" rotation="0 0 0" wand-fire visible="false"></a-entity>
<!-- 敵スポーン用エリア -->
<a-entity id="enemy-spawn"></a-entity>
<!-- サウンド設定 -->
<a-entity id="sword-sound" sound="src: url(sword-swing.mp3); on: none"></a-entity>
<a-entity id="pickup-sound" sound="src: url(pickup.mp3); on: none"></a-entity>
<a-entity id="wand-sound" sound="src: url(wand-fire.mp3); on: none"></a-entity>
<!-- インストラクション表示(初回のみ) -->
<a-entity id="instructions" position="0 2 -3">
<a-text value="Controls: Oculus Touch / Gamepad / WASD+Mouse クリックで剣を拾い、剣をクリックで振る Pキーでポーズ / Uキーでアップグレード"
align="center" color="#FFF" width="6"></a-text>
</a-entity>
<!-- ウェーブ管理 -->
<a-entity wave-manager></a-entity>
</a-scene>
<script>
/************ ゲームデータ管理 ************/
var gameData = {
score: 0,
playerHealth: 100,
wave: 1,
playerLevel: 1,
experience: 0,
currentWeapon: "None",
swordDamage: 50,
maxHealth: 100,
hasSword: false,
paused: false,
gameState: "menu" // "menu", "playing", "paused", "gameover"
};
/************ HUD 更新関数 ************/
function updateHUD() {
document.querySelector('#scoreText').setAttribute('value', 'Score: ' + gameData.score);
document.querySelector('#healthText').setAttribute('value', 'Health: ' + gameData.playerHealth);
document.querySelector('#waveText').setAttribute('value', 'Wave: ' + gameData.wave);
document.querySelector('#levelText').setAttribute('value', 'Lv: ' + gameData.playerLevel + ' Exp: ' + gameData.experience);
document.querySelector('#weaponText').setAttribute('value', 'Weapon: ' + gameData.currentWeapon);
}
/************ ゲームオーバーチェック ************/
function checkGameOver() {
if (gameData.playerHealth <= 0) {
gameData.gameState = "gameover";
document.getElementById('gameOverOverlay').style.display = "flex";
document.getElementById('finalScore').innerText = "Final Score: " + gameData.score;
}
}
/************ 経験値加算&レベルアップ ************/
function addExperience(exp) {
gameData.experience += exp;
if (gameData.experience >= 100) {
gameData.experience -= 100;
gameData.playerLevel++;
gameData.playerHealth = Math.min(gameData.maxHealth, gameData.playerHealth + 20);
openUpgradeStore();
}
updateHUD();
}
/************ 敵撃破時の演出 ************/
function killEnemy(enemy) {
if (!enemy) return;
let healthBar = enemy.querySelector('.health-bar');
if (healthBar) { healthBar.parentNode.removeChild(healthBar); }
let explosion = document.createElement('a-entity');
explosion.setAttribute('particle-system', 'preset: dust; particleCount: 100; color: #FFAA00, #FF0000;');
explosion.setAttribute('position', enemy.getAttribute('position'));
enemy.parentNode.appendChild(explosion);
setTimeout(function(){ if(explosion.parentNode) explosion.parentNode.removeChild(explosion); }, 1000);
enemy.setAttribute('animation', 'property: scale; to: 0 0 0; dur: 500; easing: easeInOutQuad');
setTimeout(function(){ if(enemy.parentNode) enemy.parentNode.removeChild(enemy); }, 500);
}
/************ カメラシェイク ************/
function cameraShake() {
let camera = document.querySelector('a-camera');
if (!camera) return;
let origPos = camera.getAttribute('position');
let shakePos = {
x: origPos.x + (Math.random()-0.5)*0.1,
y: origPos.y + (Math.random()-0.5)*0.1,
z: origPos.z
};
camera.setAttribute('position', shakePos);
setTimeout(function(){ camera.setAttribute('position', origPos); }, 100);
}
/************ アップグレードストア ************/
function openUpgradeStore() {
document.getElementById('upgradeOverlay').style.display = "flex";
gameData.paused = true;
}
function closeUpgradeStore() {
document.getElementById('upgradeOverlay').style.display = "none";
gameData.paused = false;
}
document.getElementById('closeUpgrade').addEventListener('click', closeUpgradeStore);
/************ メインメニュー&リスタート ************/
document.getElementById('startButton').addEventListener('click', function(){
document.getElementById('mainMenuOverlay').style.display = "none";
gameData.gameState = "playing";
});
document.getElementById('restartButton').addEventListener('click', function(){
window.location.reload();
});
/************ キー操作 ************/
document.addEventListener('keydown', function(e) {
if(e.key.toLowerCase() === 'p') {
gameData.paused = !gameData.paused;
document.getElementById('pauseOverlay').style.display = gameData.paused ? "flex" : "none";
}
if(e.key === 'u' && gameData.gameState === "playing" && !gameData.paused) {
openUpgradeStore();
}
if(document.getElementById('upgradeOverlay').style.display === "flex") {
if(e.key === '1') {
if(gameData.score >= 50) {
gameData.swordDamage += 10;
gameData.score -= 50;
updateHUD();
}
}
if(e.key === '2') {
if(gameData.score >= 50) {
gameData.maxHealth += 20;
gameData.score -= 50;
updateHUD();
}
}
}
});
/************ pickup コンポーネント ************/
AFRAME.registerComponent('pickup', {
init: function() {
let el = this.el;
el.addEventListener('click', function () {
if(gameData.paused || gameData.gameState !== "playing") return;
let player = document.querySelector('#player');
let playerPos = new THREE.Vector3();
player.object3D.getWorldPosition(playerPos);
let itemPos = new THREE.Vector3();
el.object3D.getWorldPosition(itemPos);
if(playerPos.distanceTo(itemPos) < 2) {
if(!gameData.hasSword) {
let pickupSound = document.querySelector('#pickup-sound');
if(pickupSound && pickupSound.components.sound) {
pickupSound.components.sound.playSound();
}
let rightHand = document.querySelector('#rightHand');
if(rightHand) {
rightHand.appendChild(el);
el.setAttribute('position', '0 0 0');
} else {
player.appendChild(el);
el.setAttribute('position', '0.3 0 -0.5');
}
gameData.currentWeapon = "Sword";
gameData.hasSword = true;
updateHUD();
console.log("Sword picked up!");
el.removeAttribute('animation');
} else {
console.log("Already holding a sword.");
}
}
});
}
});
/************ enemy-ai コンポーネント ************/
AFRAME.registerComponent('enemy-ai', {
schema: {
speed: {type: 'number', default: 0.02},
damage: {type: 'number', default: 5}
},
init: function() { this.attackCooldown = 0; },
tick: function(time, timeDelta) {
if(gameData.paused) return;
let player = document.querySelector('#player');
if(!player) return;
let enemy = this.el;
let enemyPos = enemy.object3D.position;
let playerPos = player.object3D.position;
let direction = new THREE.Vector3().subVectors(playerPos, enemyPos);
let distance = direction.length();
if(distance > 0.1) {
direction.normalize();
enemy.object3D.position.add(direction.multiplyScalar(this.data.speed * (timeDelta/16)));
}
if(distance < 1 && this.attackCooldown <= 0) {
gameData.playerHealth -= this.data.damage;
updateHUD();
cameraShake();
checkGameOver();
this.attackCooldown = 1000;
} else {
this.attackCooldown -= timeDelta;
}
}
});
/************ enemy-health コンポーネント ************/
AFRAME.registerComponent('enemy-health', {
schema: {
hp: {type: 'number', default: 100},
maxHp: {type: 'number', default: 100}
},
init: function(){
let bar = document.createElement('a-plane');
bar.setAttribute('class', 'health-bar');
bar.setAttribute('width', '1');
bar.setAttribute('height', '0.1');
bar.setAttribute('color', 'green');
bar.setAttribute('position', '0 0.8 0');
this.el.appendChild(bar);
},
updateHealthBar: function(){
let healthBar = this.el.querySelector('.health-bar');
if(healthBar) {
let hp = this.data.hp, max = this.data.maxHp;
let scaleX = Math.max(0, hp/max);
healthBar.setAttribute('scale', `${scaleX} 1 1`);
let color = (scaleX > 0.5) ? "green" : (scaleX > 0.2 ? "yellow" : "red");
healthBar.setAttribute('color', color);
}
}
});
/************ sword-swing コンポーネント ************/
AFRAME.registerComponent('sword-swing', {
init: function(){
let sword = this.el;
let self = this;
sword.addEventListener('triggerdown', function(){ self.swing(); });
sword.addEventListener('click', function(){ self.swing(); });
},
swing: function(){
if(gameData.paused) return;
this.el.emit('swing');
let soundEl = document.querySelector('#sword-sound');
if(soundEl && soundEl.components.sound){
soundEl.components.sound.playSound();
}
let swordPos = new THREE.Vector3();
this.el.object3D.getWorldPosition(swordPos);
let enemies = document.querySelectorAll('.enemy');
enemies.forEach(function(enemy){
let enemyPos = new THREE.Vector3();
enemy.object3D.getWorldPosition(enemyPos);
if(swordPos.distanceTo(enemyPos) < 1){
let eh = enemy.getAttribute('enemy-health');
eh.hp -= gameData.swordDamage;
enemy.setAttribute('enemy-health', 'hp', eh.hp);
enemy.components['enemy-health'].updateHealthBar();
if(eh.hp <= 0){
killEnemy(enemy);
gameData.score += 10;
addExperience(20);
} else {
enemy.setAttribute('material', 'color', '#ff4444');
setTimeout(function(){ enemy.setAttribute('material', 'color', '#66ff66'); }, 200);
}
updateHUD();
}
});
}
});
/************ wand-fire コンポーネント ************/
AFRAME.registerComponent('wand-fire', {
init: function(){
let wand = this.el;
let self = this;
wand.addEventListener('triggerdown', function(){ self.fire(); });
wand.addEventListener('click', function(){ self.fire(); });
},
fire: function(){
if(gameData.paused) return;
let wandSound = document.querySelector('#wand-sound');
if(wandSound && wandSound.components.sound){
wandSound.components.sound.playSound();
}
let projectile = document.createElement('a-sphere');
projectile.setAttribute('radius', '0.1');
projectile.setAttribute('color', 'orange');
let startPos = new THREE.Vector3();
this.el.object3D.getWorldPosition(startPos);
projectile.setAttribute('position', startPos);
projectile.setAttribute('projectile', '');
this.el.sceneEl.appendChild(projectile);
}
});
/************ projectile コンポーネント ************/
AFRAME.registerComponent('projectile', {
schema: { speed: {type: 'number', default: 0.1}, damage: {type: 'number', default: 30} },
init: function(){
this.direction = new THREE.Vector3();
this.el.object3D.getWorldDirection(this.direction);
},
tick: function(time, timeDelta){
if(gameData.paused) return;
let distance = this.data.speed * (timeDelta/16);
this.el.object3D.position.add(this.direction.clone().multiplyScalar(distance));
let projectilePos = new THREE.Vector3();
this.el.object3D.getWorldPosition(projectilePos);
let enemies = document.querySelectorAll('.enemy');
for(let i=0; i<enemies.length; i++){
let enemy = enemies[i];
let enemyPos = new THREE.Vector3();
enemy.object3D.getWorldPosition(enemyPos);
if(projectilePos.distanceTo(enemyPos) < 0.5){
let eh = enemy.getAttribute('enemy-health');
eh.hp -= this.data.damage;
enemy.setAttribute('enemy-health', 'hp', eh.hp);
enemy.components['enemy-health'].updateHealthBar();
if(eh.hp <= 0){
killEnemy(enemy);
gameData.score += 10;
addExperience(20);
} else {
enemy.setAttribute('material', 'color', '#ff4444');
setTimeout(function(){ enemy.setAttribute('material', 'color', '#66ff66'); }, 200);
}
updateHUD();
this.el.parentNode.removeChild(this.el);
return;
}
}
if(projectilePos.length() > 50){
this.el.parentNode.removeChild(this.el);
}
}
});
/************ weapon-switcher コンポーネント ************/
AFRAME.registerComponent('weapon-switcher', {
init: function(){
window.addEventListener('keydown', function(event){
if(event.key === '1'){
if(gameData.hasSword){
gameData.currentWeapon = "Sword";
document.querySelector('#rightHand').setAttribute('visible', 'true');
document.querySelector('#magicWand').setAttribute('visible', 'false');
}
updateHUD();
} else if(event.key === '2'){
gameData.currentWeapon = "Magic";
document.querySelector('#sword').setAttribute('visible', 'false');
document.querySelector('#magicWand').setAttribute('visible', 'true');
updateHUD();
}
});
}
});
/************ wave-manager コンポーネント ************/
// 敵が全滅したら次のウェーブを生成。ウェーブ番号が5の倍数の場合はボス出現。
AFRAME.registerComponent('wave-manager', {
tick: function(){
if(gameData.paused) return;
let spawnZone = document.querySelector('#enemy-spawn');
if(spawnZone.children.length === 0){
gameData.wave += 1;
updateHUD();
this.spawnWave();
}
},
spawnWave: function(){
let spawnZone = document.querySelector('#enemy-spawn');
if(gameData.wave % 5 === 0){
// ボスウェーブ
let boss = document.createElement('a-entity');
boss.classList.add('enemy');
boss.setAttribute('position', '0 1 -6');
boss.setAttribute('geometry', 'primitive: sphere; radius: 1');
boss.setAttribute('material', 'color: #aa0000; opacity: 0.9; transparent: true');
boss.setAttribute('animation__rotate', 'property: rotation; to: 0 360 0; dur: 6000; loop: true');
boss.setAttribute('enemy-ai', 'speed: 0.015; damage: 10');
boss.setAttribute('enemy-health', 'hp: 300; maxHp: 300');
spawnZone.appendChild(boss);
} else {
let enemyCount = 3 + gameData.wave - 1;
for(let i=0; i<enemyCount; i++){
let enemy = document.createElement('a-entity');
enemy.classList.add('enemy');
let angle = Math.random() * Math.PI * 2;
let radius = 5 + Math.random() * 5;
let x = Math.cos(angle) * radius;
let z = Math.sin(angle) * radius;
enemy.setAttribute('position', `${x} 1 ${z}`);
enemy.setAttribute('geometry', 'primitive: sphere; radius: 0.5');
enemy.setAttribute('material', 'color: #66ff66; opacity: 0.8; transparent: true');
enemy.setAttribute('animation__wobble', 'property: scale; to: 1.1 0.9 1.1; dur: 1000; dir: alternate; loop: true');
enemy.setAttribute('enemy-ai', 'speed: 0.02; damage: 5');
enemy.setAttribute('enemy-health', 'hp: 100; maxHp: 100');
spawnZone.appendChild(enemy);
}
}
}
});
/************ 初回 HUD 更新 & インストラクション削除 ************/
document.addEventListener('DOMContentLoaded', function(){
updateHUD();
setTimeout(function(){
let instructions = document.getElementById('instructions');
if(instructions){ instructions.parentNode.removeChild(instructions); }
}, 5000);
});
</script>
</body>
</html>
Automation Studio
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Automation Studio</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
<style>
:root {
--primary-color: #00c7b7;
--secondary-color: #2d3436;
--background-color: #f8f9fa;
--card-shadow: 0 4px 6px rgba(0,0,0,0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: 'Segoe UI', system-ui;
}
body {
background: var(--background-color);
color: var(--secondary-color);
}
/* ヘッダー */
.dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
background: white;
box-shadow: var(--card-shadow);
}
.brand {
display: flex;
align-items: center;
gap: 1rem;
}
.brand-logo {
width: 40px;
height: 40px;
}
/* メインコンテンツ */
.main-container {
max-width: 1200px;
margin: 2rem auto;
padding: 0 1rem;
}
/* アプレットカード */
.applet-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.applet-card {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: var(--card-shadow);
transition: transform 0.2s;
position: relative;
}
.applet-card:hover {
transform: translateY(-5px);
}
.applet-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.toggle-switch {
position: relative;
display: inline-block;
width: 48px;
height: 24px;
}
.toggle-slider {
position: absolute;
cursor: pointer;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #ccc;
transition: .4s;
border-radius: 34px;
}
.toggle-slider:before {
position: absolute;
content: "";
height: 16px;
width: 16px;
left: 4px;
bottom: 4px;
background-color: white;
transition: .4s;
border-radius: 50%;
}
input:checked + .toggle-slider {
background-color: var(--primary-color);
}
input:checked + .toggle-slider:before {
transform: translateX(24px);
}
.service-flow {
display: flex;
align-items: center;
gap: 1rem;
margin: 1.5rem 0;
}
.service-icon {
width: 48px;
height: 48px;
border-radius: 12px;
padding: 8px;
box-shadow: var(--card-shadow);
}
.divider-arrow {
flex-grow: 1;
border-top: 2px dashed #ddd;
position: relative;
}
.divider-arrow::after {
content: "➔";
position: absolute;
right: -10px;
top: -12px;
background: white;
padding: 0 4px;
color: var(--primary-color);
}
/* 作成モーダル */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-content {
background: white;
border-radius: 16px;
width: 90%;
max-width: 600px;
max-height: 90vh;
overflow-y: auto;
padding: 2rem;
animation: modalSlideIn 0.3s ease-out;
}
@keyframes modalSlideIn {
from { transform: translateY(20px); opacity: 0; }
to { transform: translateY(0); opacity: 1; }
}
.service-picker {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 1rem;
margin: 2rem 0;
}
.service-option {
text-align: center;
padding: 1rem;
border: 2px solid #eee;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.service-option:hover {
border-color: var(--primary-color);
background: #f7f7f7;
}
.service-option img {
width: 64px;
height: 64px;
margin-bottom: 0.5rem;
}
/* レスポンシブデザイン */
@media (max-width: 768px) {
.applet-grid {
grid-template-columns: 1fr;
}
.service-picker {
grid-template-columns: repeat(2, 1fr);
}
}
</style>
</head>
<body>
<header class="dashboard-header">
<div class="brand">
<img src="https://img.icons8.com/color/96/automation.png" class="brand-logo" alt="Logo">
<h1>Automation Studio</h1>
</div>
<button class="create-button" onclick="openModal()">
<i class="fas fa-plus"></i> New Automation
</button>
</header>
<div class="main-container">
<div class="applet-grid">
<!-- アプレットが動的に追加されます -->
</div>
</div>
<!-- 作成モーダル -->
<div class="modal-overlay" id="modal">
<div class="modal-content">
<h2>Create New Automation</h2>
<div class="service-picker" id="triggerServices">
<!-- トリガーサービスが動的に追加されます -->
</div>
<div class="service-picker" id="actionServices">
<!-- アクションサービスが動的に追加されます -->
</div>
<div class="modal-actions">
<button class="btn-secondary" onclick="closeModal()">Cancel</button>
<button class="btn-primary" onclick="createApplet()">Create</button>
</div>
</div>
</div>
<script>
// ダミーデータ
const services = {
triggers: [
{ id: 'email', name: 'Email', icon: 'https://img.icons8.com/color/96/new-post.png' },
{ id: 'calendar', name: 'Calendar', icon: 'https://img.icons8.com/color/96/google-calendar.png' },
{ id: 'weather', name: 'Weather', icon: 'https://img.icons8.com/color/96/partly-cloudy-day.png' }
],
actions: [
{ id: 'slack', name: 'Slack', icon: 'https://img.icons8.com/color/96/slack.png' },
{ id: 'drive', name: 'Google Drive', icon: 'https://img.icons8.com/color/96/google-drive.png' },
{ id: 'notification', name: 'Notification', icon: 'https://img.icons8.com/color/96/appointment-reminders.png' }
]
};
// モーダル制御
function openModal() {
document.getElementById('modal').style.display = 'flex';
populateServices();
}
function closeModal() {
document.getElementById('modal').style.display = 'none';
}
// サービスの表示
function populateServices() {
const triggerContainer = document.getElementById('triggerServices');
const actionContainer = document.getElementById('actionServices');
triggerContainer.innerHTML = services.triggers.map(service => `
<div class="service-option" onclick="selectTrigger('${service.id}')">
<img src="${service.icon}" alt="${service.name}">
<div>${service.name}</div>
</div>
`).join('');
actionContainer.innerHTML = services.actions.map(service => `
<div class="service-option" onclick="selectAction('${service.id}')">
<img src="${service.icon}" alt="${service.name}">
<div>${service.name}</div>
</div>
`).join('');
}
// アプレット作成
let selectedTrigger = null;
let selectedAction = null;
function selectTrigger(serviceId) {
selectedTrigger = services.triggers.find(s => s.id === serviceId);
updateSelections();
}
function selectAction(serviceId) {
selectedAction = services.actions.find(s => s.id === serviceId);
updateSelections();
}
function updateSelections() {
document.querySelectorAll('.service-option').forEach(el => {
el.style.borderColor = '#eee';
});
if (selectedTrigger) {
const triggerEl = document.querySelector(`[onclick="selectTrigger('${selectedTrigger.id}')"]`);
triggerEl.style.borderColor = 'var(--primary-color)';
}
if (selectedAction) {
const actionEl = document.querySelector(`[onclick="selectAction('${selectedAction.id}')"]`);
actionEl.style.borderColor = 'var(--primary-color)';
}
}
function createApplet() {
if (!selectedTrigger || !selectedAction) return;
const appletGrid = document.querySelector('.applet-grid');
const newApplet = document.createElement('div');
newApplet.className = 'applet-card';
newApplet.innerHTML = `
<div class="applet-header">
<h3>${selectedTrigger.name} → ${selectedAction.name}</h3>
<label class="toggle-switch">
<input type="checkbox">
<span class="toggle-slider"></span>
</label>
</div>
<div class="service-flow">
<img src="${selectedTrigger.icon}" class="service-icon">
<div class="divider-arrow"></div>
<img src="${selectedAction.icon}" class="service-icon">
</div>
<p>Last triggered: Never</p>
`;
appletGrid.appendChild(newApplet);
closeModal();
selectedTrigger = null;
selectedAction = null;
}
// モーダル外をクリックで閉じる
window.onclick = function(event) {
if (event.target.classList.contains('modal-overlay')) {
closeModal();
}
}
</script>
</body>
</html>