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}
${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();
}
}
});
}
});