イクチオカップ

みんなでチクろう! Tikuru24

https://github.com/AliceWonerfulWorld/Ikutio_AllStars

Next.js

TypeScript

PostgreSQL

PWA

TailwindCSS

24時間で投稿が消える、今この瞬間だけを共有できる新しいSNS

アリス

Maaakia

さくらんぼ

わしじゃよ

UUKINO

推しアイデア

・投稿が24時間で消えるという使用は、ツイ廃に刺さる! ・自分の顔や描いたイラストがそのままリアクションとして使える! ・複数人で通話可能!

作った背景

・ツイ廃を量産したいと考えたから。 ・24時間で消えるなら、普段つぶやけない闇を吐き出せるのではないかと考えたから。

推し技術

・PWAとプッシュ通知! CloudFlareR2で10GBまで画像保存可能! ・WebSocketを使った複数人での通話を実現! ・PWA対応で、ネイティブアプリのような使用感を実現。

プロジェクト詳細

1. プロダクトの目的

  • 「今」をチクることで今しかできない共有をコンセプトにSNSをつくっています。
  • 「短文ポスト」を中心とした軽量SNS体験を Next.js × Supabase ベースで実装
  • リアルタイム更新、ブックマーク/いいね、簡易PWA化で“すぐ使える”体験を提供
  • Gemini 連携(Grok/Glok画面)で、ユーザー履歴を踏まえた応答を生成

2.主要機能

構成図 image

ユーザー認証(Sign up / Sign in / Sign out)

チクる(投稿)/チクり一覧(投稿一覧)/自動消滅カウントダウン表示/時間経過で文字が消える

チクった投稿は24時間で消えてしまう.....その前に見なきゃ! image

時間計算のロジック

function getRemainingTime(createdAt: string) { const created = new Date(createdAt).getTime(); const now = Date.now(); const expires = created + 24 * 60 * 60 * 1000; // 24時間後 const diff = expires - now; if (diff <= 0) return null; // 期限切れ const hours = Math.floor(diff / (1000 * 60 * 60)); const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60)); const seconds = Math.floor((diff % (1000 * 60)) / 1000); return `${hours.toString().padStart(2, "0")}:${minutes .toString() .padStart(2, "0")}:${seconds.toString().padStart(2, "0")}`; }

残り時間の表示も自動で更新

const timerRef = useRef<NodeJS.Timeout | null>(null); useEffect(() => { if (timerRef.current) clearInterval(timerRef.current); timerRef.current = setInterval(() => { setPosts((prev) => [...prev]); // 強制再レンダリング }, 1000); // 1秒ごと更新 return () => { if (timerRef.current) clearInterval(timerRef.current); }; }, []);

某SNSのようにみんなのチクった投稿が一覧で表示される! image

いいね/ブックマーク、数の集計

スタンプをお絵描きして作成!

image

通知/メッセージ/検索/プロフィール/設定

image

PWA 対応(マニフェスト/簡易 Service Worker)

clock

→GiminiのAPIを用いて生成AIを実装 image localstorageでチャットの履歴を保存

const userHistoryKey = user ? `${LS_KEY}_${user.id}` : LS_KEY;

Sキーで流れ星を呼ぶ

const starfieldRef = useRef<StarfieldRef>(null); useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key.toLowerCase() === 's' || event.code === 'KeyS') { const active = document.activeElement; if (active && (active.tagName === 'INPUT' || active.tagName === 'TEXTAREA')) return; event.preventDefault(); starfieldRef.current?.triggerShootingStars(); } }; document.addEventListener('keydown', handleKeyDown); window.addEventListener('keydown', handleKeyDown); return () => { document.removeEventListener('keydown', handleKeyDown); window.removeEventListener('keydown', handleKeyDown); }; }, []);

メッセージ機能

誰とチャットするか選択 image

↓チャット画面
image

プロフィール画面

image ↓プロフィール編集画面 image

ブックマーク画面 image

天気Yohoo!投稿

Google Maps APIで地図を表示、コメントをSupabaseに保存

地図上に天気、気温、コメント等を投稿 image

ある程度ズームするとコメントを表示する機能

const show = (map.getZoom() ?? 0) >= ZOOM_FOR_COMMENT; posts .filter((p) => typeof p.lat === "number" && typeof p.lng === "number") .forEach((p) => { const text = truncate(p.comment, 24); const ov = new Ctor(new google.maps.LatLng(p.lat!, p.lng!), text); ov.setMap(map); ov.setVisible(show); overlaysRef.current.push(ov); }); } function toggleOverlayVisibility() { const zoom = mapObj.current?.getZoom(); if (zoom == null) return; const show = zoom >= ZOOM_FOR_COMMENT; overlaysRef.current.forEach((o) => o.setVisible(show)); }

地図をクリックした位置の緯度経度を送る機能

map.addListener("click", (ev: google.maps.MapMouseEvent) => { const lat = ev.latLng?.lat(); const lng = ev.latLng?.lng(); if (lat == null || lng == null) return; onMapClick({ lat, lng }); });

Realction

撮影した画像をCloudflare R2にアップロード、画像のURLをSupabaseに保存

↓リアクションを撮る image ↓リアクションスタンプとして使用 image

Cloudflare R2へアップロードし、URLをSpabaseに保存する機能

126 const upload = async () => { 127 setMsg(null); 128 setUploading(true); 129 try { 130 const { data: u } = await supabase.auth.getUser(); 131 const userId = u.user?.id; 132 if (!userId) throw new Error("ログインが必要です(authユーザーのみ保存可)"); 139 const c = canvasRef.current!; 140 const blob: Blob = await new Promise((res, rej) => 141 c.toBlob((b) => (b ? res(b) : rej(new Error("toBlob失敗"))), "image/jpeg", 0.92) 142 ); 145 // R2へアップロード 146 const formData = new FormData(); 147 formData.append("file", blob, "reaction.jpg"); 148 formData.append("userId", userId); 149 const res = await fetch("/api/upload-reaction", { method: "POST", body: formData }); 150 const result = await res.json(); 151 if (!res.ok) throw new Error(result.error || "R2アップロード失敗"); 152 const imageUrl = result.url; 154 // SupabaseにURL保存 155 const { error } = await supabase.from("make_stamp").insert({ make_stanp_url: imageUrl }); 156 if (error) throw error; 157 setMsg("保存しました"); 158 await fetchRecent(); 159 } catch (e: any) { 160 setMsg(`保存に失敗: ${e?.message ?? e}`); 161 } finally { 162 setUploading(false); 163 } 164 };

TikuriBAR (T〇itterでいうス〇ースみたいなやつ)

営業中のBARを選んで image

みんなで楽しくおしゃべり image

WebSocketサーバーはRailwayでデプロイしました! image

ユーザー権限システムとBAR管理システム

interface BarRoom { id: string; // BAR固有ID title: string; // BAR名 users: Map<string, BarUser>; // 参加ユーザー createdAt: number; // 作成時刻 } interface BarUser { id: string; username: string; role: 'bartender' | 'speaker' | 'listener'; // 権限レベル isMuted: boolean; // ミュート状態 ws: WebSocket; // 個別WebSocket接続 }

リアルタイム状態管理

case 'user_joined': setUsers(prev => [...prev, data.user]); // 新ユーザー追加 case 'user_left': setUsers(prev => prev.filter(u => u.id !== data.user.id)); // ユーザー削除 case 'audio_chunk': handleAudioChunk(data); // 音声データ処理

音声キャプチャ設定

// 高品質音声設定 const audioConfig = { sampleRate: 16000, // 16kHz サンプリング channelCount: 1, // モノラル echoCancellation: true, // エコー除去 noiseSuppression: true, // ノイズ除去 autoGainControl: true // 自動ゲイン制御 }; // MediaRecorder設定 const mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus', // Opusコーデック audioBitsPerSecond: 32000 // 32kbps });

PWA対応

↓ネイティブアプリのようなUIで使用可能 image ↓スマートフォンでも可能 image
image

プッシュ通知

プッシュ通知の処理
self.addEventListener('push', (event) => { let pushData = {}; if (event.data) { try { pushData = event.data.json(); options.body = pushData.body || options.body; options.title = pushData.title || 'Ikutio AllStars'; } catch (e) { console.log('Push data is not JSON:', e); } } event.waitUntil( self.registration.showNotification(options.title, options) ); });

にんじゃわんこによるお気持ち表明

image 圧が凄い......

めっちゃきれい

image

使用技術

  • フロント: Next.js/React/Tailwind CSS
  • 認証/DB: Supabase
  • AI: Google Generative AI(Gemini 2.0 Flash)
  • ストレージ: Cloudflare R2
  • UI アイコン: lucide-react
  • PWA: Web App Manifest + Service Worker(キャッシュ)
  • 地図: Google Map
  • 認証/OAuth ・Google認証: OAuth 2.0 + OpenID Connect ・X認証: OAuth(Supabase Auth 連携)
  • Tikurubar ・WebSocket ・Railway/Render(クラウドデプロイ)

アーキテクチャー図

アリス

@Wazapalice