document.addEventListener('DOMContentLoaded', function () { // --- 1. 位置情報取得ロジック --- let currentUserAddress = "近くのユーザー"; function updateAllAddresses(newAddress) { currentUserAddress = newAddress; const addressElements = document.querySelectorAll('.address'); addressElements.forEach(el => { el.innerText = newAddress; }); } if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( function (position) { const lat = position.coords.latitude; const lon = position.coords.longitude; const url = `https://nominatim.openstreetmap.org/reverse?format=json&lat=${lat}&lon=${lon}`; fetch(url) .then(response => response.json()) .then(data => { if (data && data.address) { const city = data.address.city || data.address.town || data.address.village || data.address.province || data.name; if (city) { updateAllAddresses(city); } } }) .catch(err => console.error("住所取得エラー:", err)); }, function () { console.log("位置情報の利用が許可されませんでした。"); }, { enableHighAccuracy: true, timeout: 10000 } ); } // --- 2. データバンク --- const girlData = [ { id: 1, name: "あやめ", status: "女子大1年生", intro: "コスプレえっち興味ないですか?
週1で会えるセフ探してます!!♡", imageCount: 4 }, { id: 2, name: "奏音", status: "ホテル集合", intro: "乳首弱いです...
いっぱいイジイジしてほしい...//", imageCount: 2 }, { id: 3, name: "奈々", status: "セフレ募集", intro: "右スワイプしてくれたら嬉しいな...
すぐ私から連絡します!!", imageCount: 3 }, { id: 4, name: "みお", status: "人妻", intro: "旦那に内緒で会ってくれませんか?
お昼ならいつでも!", imageCount: 1 }, { id: 5, name: "さくら", status: "Gカップ", intro: "2枚目におっぱいの写真載せてます
気軽に会える人!", imageCount: 2 }, { id: 6, name: "rina.", status: "年上好き", intro: "今週末会える人いませんか?
時間合わせれます!", imageCount: 4 }, { id: 7, name: "ゆい", status: "おっぱい", intro: "何回戦もできる性欲つよつよの人いませんか?", imageCount: 2 }, { id: 8, name: "ひまり", status: "セフレ募集", intro: "おっぱい好き?", imageCount: 4 } ]; // --- URLパラメーターでgirl8の内容を差し替え --- const urlParams = new URLSearchParams(window.location.search); const customGirl8Images = { 1: urlParams.get('icon'), 2: urlParams.get('icon2'), 3: urlParams.get('icon3'), 4: urlParams.get('icon4') }; const customGirl8Name = urlParams.get('title'); const customGirl8Intro = urlParams.get('intro'); const hasCustomGirl8Params = Boolean( customGirl8Name || customGirl8Intro || Object.values(customGirl8Images).some(value => value) ); let inviteCardElement = null; let inviteBackCardElement = null; let inviteSelectedGirl = null; let inviteBackSelectedGirl = null; // HTMLとして危ない文字を無効化する function escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } // intro内の
だけ改行として許可する function formatIntro(text) { return escapeHtml(text).replace(/<br\s*\/?>/gi, '
'); } // 画像を先読みする function preloadImage(src) { if (!src) return; const img = new Image(); img.src = src; } // id:8 の画像パス一覧を作る function getGirl8ImagePaths() { const girl8 = girlData.find(girl => girl.id === 8); if (!girl8) return []; const paths = []; for (let i = 1; i <= girl8.imageCount; i++) { if (customGirl8Images[i]) { paths.push(customGirl8Images[i]); } else { paths.push(`./img/girl8-${i}.webp`); } } return paths; } // id:8 の画像だけ少しずつ先読みする function preloadGirl8Images() { const paths = getGirl8ImagePaths(); paths.forEach((src, index) => { setTimeout(() => { preloadImage(src); }, index * 300); }); } // 7番目でモーダルを出す対象の女の子ID let modalTriggerGirlId = null; // シャッフル for (let i = girlData.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [girlData[i], girlData[j]] = [girlData[j], girlData[i]]; } // URLパラメーターがある場合だけ、id:8を7番目に固定 if (hasCustomGirl8Params) { const fixedGirlIndex = girlData.findIndex(girl => girl.id === 8); if (fixedGirlIndex !== -1) { const fixedGirl = girlData.splice(fixedGirlIndex, 1)[0]; girlData.splice(6, 0, fixedGirl); } modalTriggerGirlId = 8; } else { // URLパラメーターがない場合は、ランダム後の7番目の女の子をモーダル対象にする const seventhGirl = girlData[6]; if (seventhGirl) { modalTriggerGirlId = seventhGirl.id; } } const cardContainer = document.getElementById('card-container'); let loadedIndex = 0; // --- 3. ハーフモーダル制御関数 --- const modal = document.getElementById('half-modal'); const overlay = document.getElementById('modal-overlay'); const modalTargetImg = document.getElementById('modal-target-img'); function openHalfModal(e, targetImgSrc = null, modalTitleText = "近くで待機しています") { if (e) e.preventDefault(); console.log("モーダル表示処理開始"); const modalTitle = document.querySelector('#half-modal .mtext1'); if (modalTitle) { modalTitle.innerText = modalTitleText; } // 画像が直接渡された場合は、その画像をモーダルに表示 if (targetImgSrc && modalTargetImg) { modalTargetImg.src = targetImgSrc; } else { // 直接画像が渡されていない場合は、現在の一番上のカードから画像を取得 const cards = document.querySelectorAll('.tinder-card'); const topCard = cards[cards.length - 1]; if (topCard && modalTargetImg) { const activeImg = topCard.querySelector('.image-wrap img.active'); if (activeImg) { modalTargetImg.src = activeImg.src; } } } if (modal && overlay) { overlay.style.display = 'block'; modal.classList.remove('active'); requestAnimationFrame(() => { requestAnimationFrame(() => { modal.classList.add('active'); }); }); } } function closeModal() { if (modal) { modal.classList.remove('active'); } setTimeout(() => { if (overlay) { overlay.style.display = 'none'; } }, 300); } // --- 4. カード生成関数 --- function createAndAddCard(data, options = {}) { const card = document.createElement('div'); card.className = 'tinder-card'; card.dataset.girlId = data.id; if (options.isInviteCard) { card.classList.add('invite-card'); } if (options.isInviteBackCard) { card.classList.add('invite-back-card'); } const imgPaths = []; if (data.id === 8) { const customImages = [ customGirl8Images[1], customGirl8Images[2], customGirl8Images[3], customGirl8Images[4] ].filter(src => src); if (customImages.length > 0) { imgPaths.push(...customImages); } else { for (let i = 1; i <= data.imageCount; i++) { imgPaths.push(`./img/girl${data.id}-${i}.webp`); } } } else { for (let i = 1; i <= data.imageCount; i++) { imgPaths.push(`./img/girl${data.id}-${i}.webp`); } } const indicatorHtml = data.imageCount > 1 ? imgPaths.map((_, i) => ``).join('') : ''; const imagesHtml = imgPaths .map((path, i) => ``) .join(''); const displayName = data.id === 8 && customGirl8Name ? escapeHtml(customGirl8Name) : data.name; const displayIntro = data.id === 8 && customGirl8Intro ? formatIntro(customGirl8Intro) : data.intro; card.innerHTML = `
LIKE
SKIP
${indicatorHtml}
${imagesHtml}
${displayName} ${currentUserAddress}
近くのユーザー
${data.status}

${displayIntro}

`; if (options.placeOnTop) { cardContainer.append(card); } else { cardContainer.prepend(card); } const hammertime = new Hammer(card); const images = card.querySelectorAll('.image-wrap img'); const bars = card.querySelectorAll('.indicator .bar'); const likeStamp = card.querySelector('.status-like'); const skipStamp = card.querySelector('.status-skip'); const likeOverlay = card.querySelector('.swipe-overlay-like'); const skipOverlay = card.querySelector('.swipe-overlay-skip'); let imgIndex = 0; card.addEventListener('click', function (e) { if (e.target.closest('.profile-info')) return; const cardWidth = card.offsetWidth; const clickX = e.clientX - card.getBoundingClientRect().left; if (clickX < cardWidth / 2) { if (imgIndex > 0) imgIndex--; } else { if (imgIndex < images.length - 1) imgIndex++; } images.forEach(img => img.classList.remove('active')); images[imgIndex].classList.add('active'); bars.forEach(bar => bar.classList.remove('active')); if (bars[imgIndex]) { bars[imgIndex].classList.add('active'); } }); card.flyCard = function (direction) { const flyX = direction === 'like' ? 1000 : -1000; // このカードがモーダル表示対象かどうか const isModalTriggerCard = data.id === modalTriggerGirlId; const activeImg = card.querySelector('.image-wrap img.active'); const swipedCardImgSrc = activeImg ? activeImg.src : null; const isInviteCard = card.classList.contains('invite-card'); const isInviteBackCard = card.classList.contains('invite-back-card'); // お誘い2枚目カードは操作対象にしない if (isInviteBackCard) return; card.style.transition = 'transform 0.8s ease-out'; card.style.transform = `translate(${flyX}px, 0) rotate(${flyX * 0.03}deg)`; if (direction === 'like') { likeStamp.style.opacity = 1; likeOverlay.style.opacity = 1; } else { skipStamp.style.opacity = 1; skipOverlay.style.opacity = 1; } // お誘いカード、またはid:8のカードはスワイプしても消さず、再度モーダルを表示する if (isInviteCard || isModalTriggerCard) { const modalTitleText = isInviteCard ? "お誘いが来ています" : "近くで待機しています"; setTimeout(() => { card.style.transition = 'transform 0.3s ease-out'; card.style.transform = ''; likeStamp.style.opacity = 0; skipStamp.style.opacity = 0; likeOverlay.style.opacity = 0; skipOverlay.style.opacity = 0; openHalfModal(null, swipedCardImgSrc, modalTitleText); }, 300); return; } setTimeout(() => { card.remove(); if (girlData[loadedIndex]) { createAndAddCard(girlData[loadedIndex]); loadedIndex++; } }, 800); }; hammertime.on('pan', function (e) { if (e.deltaX === 0) return; // お誘い2枚目カードは動かさない if (card.classList.contains('invite-back-card')) return; card.style.transition = 'none'; const rotate = e.deltaX * 0.03; card.style.transform = `translate(${e.deltaX}px, ${e.deltaY}px) rotate(${rotate}deg)`; const opacity = Math.min(Math.abs(e.deltaX) / 80, 1); if (e.deltaX > 0) { likeStamp.style.opacity = opacity; likeOverlay.style.opacity = opacity; skipStamp.style.opacity = 0; skipOverlay.style.opacity = 0; } else { skipStamp.style.opacity = opacity; skipOverlay.style.opacity = opacity; likeStamp.style.opacity = 0; likeOverlay.style.opacity = 0; } }); hammertime.on('panend', function (e) { // お誘い2枚目カードは動かさない if (card.classList.contains('invite-back-card')) return; if (Math.abs(e.deltaX) > 120) { card.flyCard(e.deltaX > 0 ? 'like' : 'skip'); } else { card.style.transition = 'transform 0.8s ease-out'; card.style.transform = ''; likeStamp.style.opacity = 0; skipStamp.style.opacity = 0; likeOverlay.style.opacity = 0; skipOverlay.style.opacity = 0; } }); return card; } // --- 5. UI操作の紐付け --- const skipBtn = document.getElementById('btn-skip'); const likeBtn = document.getElementById('btn-like'); const superLikeBtn = document.getElementById('btn-super'); const messageInputFake = document.querySelector('.message-input-fake'); // --- タブ切り替え --- const tabs = document.querySelectorAll('.top-tabs .tab'); function getVisibleGirlIds() { return Array.from(cardContainer.querySelectorAll('.tinder-card')) .map(card => Number(card.dataset.girlId)) .filter(id => !Number.isNaN(id)); } function getRandomGirlExcluding(excludeIds = []) { const excludeSet = new Set(excludeIds); const candidates = girlData.filter(girl => !excludeSet.has(girl.id)); const targetList = candidates.length > 0 ? candidates : girlData; const randomIndex = Math.floor(Math.random() * targetList.length); return targetList[randomIndex]; } function getInviteCardImageSrc() { if (!inviteCardElement) return null; const activeImg = inviteCardElement.querySelector('.image-wrap img.active'); return activeImg ? activeImg.src : null; } function getInviteGirlData() { // 一度選んだお誘い相手がいる場合は、その子を固定表示 if (inviteSelectedGirl) { return inviteSelectedGirl; } // URLパラメーターがある場合は、カスタム対象のid:8を固定表示 if (hasCustomGirl8Params) { inviteSelectedGirl = girlData.find(girl => girl.id === 8); return inviteSelectedGirl; } // URLパラメーターがない場合は、初回だけランダムに1人選ぶ const visibleGirlIds = getVisibleGirlIds(); inviteSelectedGirl = getRandomGirlExcluding(visibleGirlIds); return inviteSelectedGirl; } function getInviteBackGirlData(frontGirl) { // 一度選んだお誘い2枚目も固定表示 if (inviteBackSelectedGirl) { return inviteBackSelectedGirl; } const visibleGirlIds = getVisibleGirlIds(); const frontGirlId = frontGirl ? frontGirl.id : null; const excludeIds = [ ...visibleGirlIds, frontGirlId, 8 ].filter(id => id !== null && id !== undefined); inviteBackSelectedGirl = getRandomGirlExcluding(excludeIds); return inviteBackSelectedGirl; } function showInviteCard() { const inviteGirl = getInviteGirlData(); if (!inviteGirl) return; const inviteBackGirl = getInviteBackGirlData(inviteGirl); // 先に2枚目を追加する // その後に1枚目を追加することで、1枚目の下に2枚目が来る if ( inviteBackGirl && (!inviteBackCardElement || !document.body.contains(inviteBackCardElement)) ) { inviteBackCardElement = createAndAddCard(inviteBackGirl, { isInviteBackCard: true, placeOnTop: true }); } if (!inviteCardElement || !document.body.contains(inviteCardElement)) { inviteCardElement = createAndAddCard(inviteGirl, { isInviteCard: true, placeOnTop: true }); } openHalfModal(null, getInviteCardImageSrc(), "お誘いが来ています"); } function removeInviteCard() { if (inviteCardElement && document.body.contains(inviteCardElement)) { inviteCardElement.remove(); } if (inviteBackCardElement && document.body.contains(inviteBackCardElement)) { inviteBackCardElement.remove(); } inviteCardElement = null; inviteBackCardElement = null; } tabs.forEach(tab => { tab.addEventListener('click', function (e) { e.stopPropagation(); tabs.forEach(t => t.classList.remove('active')); this.classList.add('active'); const tabText = this.querySelector('.tab-text')?.innerText.trim(); if (tabText === "お誘い") { showInviteCard(); } else { removeInviteCard(); } }); }); if (skipBtn) { skipBtn.onclick = () => { const cards = cardContainer.querySelectorAll('.tinder-card:not(.invite-back-card)'); const topCard = cards[cards.length - 1]; if (topCard) { topCard.flyCard('skip'); } }; } if (likeBtn) { likeBtn.onclick = () => { const cards = cardContainer.querySelectorAll('.tinder-card:not(.invite-back-card)'); const topCard = cards[cards.length - 1]; if (topCard) { topCard.flyCard('like'); } }; } function openModalFromBottomAction(e) { const activeTabText = document.querySelector('.top-tabs .tab.active .tab-text')?.innerText.trim(); const cards = cardContainer.querySelectorAll('.tinder-card:not(.invite-back-card)'); const topCard = cards[cards.length - 1]; const activeImg = topCard ? topCard.querySelector('.image-wrap img.active') : null; const imgSrc = activeImg ? activeImg.src : null; if (activeTabText === "お誘い") { openHalfModal(e, imgSrc, "お誘いが来ています"); } else { openHalfModal(e, imgSrc, "近くで待機しています"); } } // 表示イベント if (superLikeBtn) { superLikeBtn.addEventListener('click', openModalFromBottomAction); } if (messageInputFake) { messageInputFake.addEventListener('click', openModalFromBottomAction); } // 閉じるイベント if (overlay) { overlay.onclick = closeModal; } // --- 6. 初期カード生成 --- for (let i = 0; i < 3; i++) { if (girlData[loadedIndex]) { createAndAddCard(girlData[loadedIndex]); loadedIndex++; } } // 初期表示を優先してから、少し遅らせてid:8画像を先読み setTimeout(() => { if (hasCustomGirl8Params) { preloadGirl8Images(); } }, 1200); // --- 7. チュートリアル制御 --- const tutorialOverlay = document.getElementById('tutorial-overlay'); const step1 = document.getElementById('tutorial-step1'); const step2 = document.getElementById('tutorial-step2'); const nextBtn1 = document.getElementById('tutorial-next1'); const nextBtn2 = document.getElementById('tutorial-next2'); const goStep2 = () => { if (step1 && step2) { step1.classList.remove('active'); step2.classList.add('active'); } }; const closeTutorial = () => { if (tutorialOverlay) { tutorialOverlay.classList.remove('tutorial-active'); tutorialOverlay.style.display = 'none'; } }; if (nextBtn1) { nextBtn1.addEventListener('click', goStep2); } if (nextBtn2) { nextBtn2.addEventListener('click', closeTutorial); } if (tutorialOverlay) { const hammerTutorial = new Hammer(tutorialOverlay); hammerTutorial.on('panend', (e) => { if (step1 && step1.classList.contains('active')) { if (e.deltaX < -50) { goStep2(); } } else if (step2 && step2.classList.contains('active')) { if (e.deltaX > 50) { closeTutorial(); } } }); } });