FAQ/

カテゴリ別カラムFAQ

Preview

カテゴリごとに分類されたFAQを2カラムで表示する、整理されたアコーディオンセクション

Source Code
tsx
153 lines
1"use client";
2
3import { useState } from "react";
4
5function CornerDots({ className = "" }: { className?: string }) {
6 return (
7 <div className={`absolute h-3 w-3 ${className}`}>
8 <div className="absolute left-0 top-0 h-1.5 w-1.5 rounded-full bg-muted-foreground/40" />
9 <div className="absolute right-0 top-0 h-1.5 w-1.5 rounded-full bg-muted-foreground/40" />
10 <div className="absolute bottom-0 left-0 h-1.5 w-1.5 rounded-full bg-muted-foreground/40" />
11 <div className="absolute bottom-0 right-0 h-1.5 w-1.5 rounded-full bg-muted-foreground/40" />
12 </div>
13 );
14}
15
16interface FaqItem {
17 question: string;
18 answer: string;
19}
20
21interface FaqCategory {
22 title: string;
23 items: FaqItem[];
24}
25
26export function FaqColumns001() {
27 const [openItems, setOpenItems] = useState<Record<string, boolean>>({});
28
29 const toggleItem = (key: string) => {
30 setOpenItems((prev) => ({ ...prev, [key]: !prev[key] }));
31 };
32
33 const categories: FaqCategory[] = [
34 {
35 title: "はじめに",
36 items: [
37 {
38 question: "アカウント登録に必要なものは何ですか?",
39 answer:
40 "メールアドレスのみで登録いただけます。クレジットカードの登録は、有料プランへの移行時に必要となります。",
41 },
42 {
43 question: "無料プランの制限を教えてください",
44 answer:
45 "無料プランでは、3プロジェクトまで作成可能で、1GBのストレージをご利用いただけます。基本的な機能はすべてお使いいただけます。",
46 },
47 {
48 question: "データの移行はサポートされていますか?",
49 answer:
50 "CSV、JSON形式でのインポートに対応しています。大規模なデータ移行については、専任チームがサポートいたします。",
51 },
52 ],
53 },
54 {
55 title: "料金・プラン",
56 items: [
57 {
58 question: "支払い方法にはどのようなものがありますか?",
59 answer:
60 "クレジットカード(Visa、Mastercard、AMEX)および銀行振込に対応しています。年間契約の場合は請求書払いも可能です。",
61 },
62 {
63 question: "プランの変更はいつでもできますか?",
64 answer:
65 "はい、いつでもアップグレード・ダウングレードが可能です。差額は日割りで計算されます。",
66 },
67 {
68 question: "解約時にデータはどうなりますか?",
69 answer:
70 "解約後30日間はデータを保持します。その間にエクスポートいただくか、再契約いただければデータは維持されます。",
71 },
72 ],
73 },
74 ];
75
76 return (
77 <section className="relative bg-background py-28">
78 <CornerDots className="left-6 top-6" />
79 <CornerDots className="right-6 top-6" />
80 <CornerDots className="bottom-6 left-6" />
81 <CornerDots className="bottom-6 right-6" />
82
83 <div className="mx-auto max-w-5xl px-4 sm:px-6 lg:px-8">
84 {/* ヘッダー */}
85 <div className="mb-20">
86 <p className="text-[10px] uppercase tracking-[0.3em] text-muted-foreground">
87 FAQ
88 </p>
89 <div className="mt-4 h-px w-12 bg-border/40" />
90 <h2 className="mt-6 text-2xl font-light tracking-wide text-foreground sm:text-3xl">
91 よくあるご質問
92 </h2>
93 </div>
94
95 {/* カテゴリ別カラムレイアウト */}
96 <div className="grid grid-cols-1 gap-16 lg:grid-cols-2">
97 {categories.map((category, catIndex) => (
98 <div key={catIndex}>
99 <p className="text-xs uppercase tracking-[0.2em] text-muted-foreground">
100 {category.title}
101 </p>
102 <div className="mt-4 h-px bg-border/40" />
103
104 <div className="mt-6 space-y-0 divide-y divide-border/30">
105 {category.items.map((item, itemIndex) => {
106 const key = `${catIndex}-${itemIndex}`;
107 const isOpen = openItems[key] ?? false;
108
109 return (
110 <div key={itemIndex} className="py-5">
111 <button
112 className="flex w-full items-start justify-between text-left"
113 onClick={() => toggleItem(key)}
114 >
115 <span className="pr-6 text-sm font-light tracking-wide text-foreground">
116 {item.question}
117 </span>
118 <span className="mt-0.5 flex-shrink-0 text-muted-foreground/60">
119 <svg
120 className={`h-3.5 w-3.5 transition-transform duration-300 ${isOpen ? "rotate-45" : ""}`}
121 fill="none"
122 stroke="currentColor"
123 viewBox="0 0 24 24"
124 >
125 <path
126 strokeLinecap="round"
127 strokeLinejoin="round"
128 strokeWidth={1.5}
129 d="M12 4v16m8-8H4"
130 />
131 </svg>
132 </span>
133 </button>
134 <div
135 className={`overflow-hidden transition-all duration-300 ${
136 isOpen ? "max-h-96 pt-3" : "max-h-0"
137 }`}
138 >
139 <p className="text-sm font-light leading-relaxed text-muted-foreground/70">
140 {item.answer}
141 </p>
142 </div>
143 </div>
144 );
145 })}
146 </div>
147 </div>
148 ))}
149 </div>
150 </div>
151 </section>
152 );
153}