Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 133 additions & 0 deletions admin/next-project/seeft-admin/src/pages/prototypes/manual-preview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import React, { useState } from "react";

// 入力された Google ドキュメントの URL を iframe 用の /preview URL に変換する
// (/export?format=pdf は Content-Disposition: attachment でダウンロードになるため、表示には /preview を使う)
const toEmbedPreviewUrl = (originalUrl: string): string | null => {
const trimmed = originalUrl.trim();
if (!trimmed) return null;

try {
const url = new URL(trimmed);

if (url.hostname.includes("docs.google.com")) {
// パスからドキュメントID部分だけ残し、/preview に統一(iframe 内でその場表示される)
const match = url.pathname.match(/\/document\/d\/([^/]+)/);
if (match) {
return `https://docs.google.com/document/d/${match[1]}/preview`;
}
}

return trimmed;
} catch {
return null;
}
};

const ManualPreviewPrototype: React.FC = () => {
const [inputUrl, setInputUrl] = useState("");
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [isOpen, setIsOpen] = useState(false);
const [error, setError] = useState<string | null>(null);

const handlePreview = () => {
const converted = toEmbedPreviewUrl(inputUrl);
if (!converted) {
setError("有効な URL を入力してください。");
setPreviewUrl(null);
setIsOpen(false);
return;
}

setError(null);
setPreviewUrl(converted);
setIsOpen(true);
};

return (
<main style={{ maxWidth: 960, margin: "40px auto", padding: "0 16px" }}>
<h1 style={{ fontSize: 24, fontWeight: 600, marginBottom: 16 }}>
Google ドキュメント プレビュー(プロトタイプ)
</h1>

<p style={{ marginBottom: 12, fontSize: 14, color: "#444" }}>
共有リンク(edit や view の URL)を入力すると、
/preview に書き換えて iframe 内にその場で表示します(ダウンロードされません)。
既存機能とは独立したサンプルページです。
</p>

<div style={{ marginBottom: 16 }}>
<label style={{ display: "block", fontSize: 14, marginBottom: 4 }}>
Google ドキュメントの URL
</label>
<input
type="text"
value={inputUrl}
onChange={(e) => setInputUrl(e.target.value)}
placeholder="https://docs.google.com/document/d/..."
style={{
width: "100%",
padding: "8px 12px",
borderRadius: 4,
border: "1px solid #ccc",
fontSize: 14,
}}
/>
</div>

<button
type="button"
onClick={handlePreview}
style={{
padding: "6px 16px",
borderRadius: 4,
border: "none",
backgroundColor: "#2563eb",
color: "#fff",
fontSize: 14,
cursor: "pointer",
}}
>
プレビューを開く
</button>

{error && (
<p style={{ marginTop: 12, color: "#b91c1c", fontSize: 14 }}>{error}</p>
)}

{previewUrl && (
<section style={{ marginTop: 24 }}>
<button
type="button"
onClick={() => setIsOpen((prev) => !prev)}
style={{
padding: "4px 12px",
borderRadius: 4,
border: "1px solid #ccc",
backgroundColor: "#f3f4f6",
fontSize: 13,
cursor: "pointer",
}}
>
{isOpen ? "プレビューを閉じる" : "プレビューを表示"}
</button>

{isOpen && (
<div style={{ marginTop: 16 }}>
<iframe
src={previewUrl}
style={{
width: "100%",
height: "600px",
border: "1px solid #e5e7eb",
}}
/>
</div>
)}
</section>
)}
</main>
);
};

export default ManualPreviewPrototype;

66 changes: 66 additions & 0 deletions mobile/lib/widgets/manual_viewer.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// ignore: avoid_web_libraries_in_flutter
import 'dart:html' as html;
import 'dart:ui_web' as ui;
import 'package:flutter/material.dart';
import 'package:seeft_mobile/theme/tokens.dart';

// Google Docs / Google Slides の URL を埋め込み用に変換する
String _toEmbeddableUrl(String url) {
final uri = Uri.tryParse(url);
if (uri == null) return url;

// Google Docs / Slides / Sheets の /edit や /view を /preview に変換
if (uri.host == 'docs.google.com') {
final newPath = uri.path
.replaceFirst(RegExp(r'/edit$'), '/preview')
.replaceFirst(RegExp(r'/view$'), '/preview');
return uri.replace(path: newPath, queryParameters: {}).toString();
}

return url;
}

class ManualViewer extends StatefulWidget {
final String url;

const ManualViewer({super.key, required this.url});

@override
State<ManualViewer> createState() => _ManualViewerState();
}

class _ManualViewerState extends State<ManualViewer> {
late final String _viewId;

@override
void initState() {
super.initState();
// viewId はウィジェットごとにユニークにする
_viewId = 'manual-iframe-${widget.url.hashCode}';

ui.platformViewRegistry.registerViewFactory(_viewId, (int viewId) {
final embeddableUrl = _toEmbeddableUrl(widget.url);
return html.IFrameElement()
..src = embeddableUrl
..style.border = 'none'
..style.width = '100%'
..style.height = '100%'
..allowFullscreen = true;
});
}

@override
Widget build(BuildContext context) {
return ClipRRect(
borderRadius: BorderRadius.circular(8.0),
child: Container(
height: 480,
decoration: BoxDecoration(
border: Border.all(color: AppColors.grayLight),
borderRadius: BorderRadius.circular(8.0),
),
child: HtmlElementView(viewType: _viewId),
),
);
}
}
79 changes: 66 additions & 13 deletions mobile/lib/widgets/shift_card.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:seeft_mobile/configs/importer.dart';
import 'package:seeft_mobile/widgets/manual_viewer.dart';
import 'package:url_launcher/url_launcher.dart';

// シフトカードのウィジェット
Expand Down Expand Up @@ -232,8 +233,8 @@ class ShiftCard extends StatelessWidget {
}
// マニュアルセクションのUIを生成するヘルパーメソッド
Widget _buildManualSection({
required String title,
String? url
required String title,
String? url,
}) {
return Padding(
padding: const EdgeInsets.only(top: 8.0, bottom: 8.0),
Expand All @@ -247,24 +248,76 @@ class ShiftCard extends StatelessWidget {
style: const TextStyle(
fontSize: AppFontSizes.xs,
color: AppColors.textBlack,
fontWeight: FontWeight.bold
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4.0),
GestureDetector(
onTap: () => url != null ? _launchManualUrl(url) : null, // マニュアルが存在すればタップでマニュアルを開く
child: Text(
url ?? 'マニュアルがありません',
style: TextStyle(
if (url == null)
Text(
'マニュアルがありません',
style: const TextStyle(
fontSize: AppFontSizes.xs,
color: url != null ? AppColors.link : AppColors.textBlack,
height: 1.5 // 行間を調整
)
),
)
color: AppColors.textBlack,
height: 1.5,
),
)
else
_InlineManualExpansion(url: url),
],
),
),
);
}
}

// マニュアルをインラインで開閉するウィジェット
class _InlineManualExpansion extends StatefulWidget {
final String url;
const _InlineManualExpansion({required this.url});

@override
State<_InlineManualExpansion> createState() => _InlineManualExpansionState();
}

class _InlineManualExpansionState extends State<_InlineManualExpansion> {
bool _isExpanded = false;

@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
InkWell(
borderRadius: BorderRadius.circular(4.0),
onTap: () => setState(() => _isExpanded = !_isExpanded),
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
_isExpanded ? Icons.expand_less : Icons.expand_more,
size: 16,
color: AppColors.link,
),
const SizedBox(width: 4.0),
Text(
_isExpanded ? 'マニュアルを閉じる' : 'マニュアルを見る',
style: const TextStyle(
fontSize: AppFontSizes.xs,
color: AppColors.link,
height: 1.5,
),
),
],
),
),
),
if (_isExpanded) ...[
const SizedBox(height: 8.0),
ManualViewer(url: widget.url),
],
],
);
}
}