Youngger9765 Claude commited on
Commit
0ab885b
·
1 Parent(s): b514fa5

Add Flask web application with Docker support

Browse files

- Add Flask application (app.py)
- Add HTML template for main page
- Add requirements.txt with Flask dependency
- Add Dockerfile for containerization
- Update README with project information

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <[email protected]>

Files changed (5) hide show
  1. Dockerfile +14 -0
  2. README.md +27 -5
  3. app.py +98 -0
  4. requirements.txt +9 -0
  5. templates/index.html +259 -0
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.9
2
+
3
+ RUN useradd -m -u 1000 user
4
+ USER user
5
+ ENV PATH="/home/user/.local/bin:$PATH"
6
+
7
+ WORKDIR /app
8
+
9
+ COPY --chown=user ./requirements.txt requirements.txt
10
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
11
+
12
+ COPY --chown=user . /app
13
+
14
+ CMD ["uvicorn", "app:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,10 +1,32 @@
1
  ---
2
- title: Lsw Html Creator
3
- emoji: 📚
4
- colorFrom: red
5
- colorTo: red
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: LSW HTML Creator
3
+ emoji: 📝
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: docker
7
  pinned: false
8
  ---
9
 
10
+ # 龍顏文學獎 HTML 建立器
11
+
12
+ 這是一個專為龍顏文學獎設計的 HTML 靜態頁面生成器,具有以下功能:
13
+
14
+ 1. **HTML 模板編輯器** - 提供可自訂的 HTML 模板
15
+ 2. **變數替換系統** - 支援 Jinja2 模板語法的變數替換
16
+ 3. **Google Sheets 整合** - 自動同步歷年得獎紀錄
17
+ 4. **一鍵生成** - 點擊按鈕即可下載完整的靜態 HTML 檔案
18
+
19
+ ## 環境變數設定
20
+
21
+ 在 Hugging Face Spaces 設定中加入以下環境變數:
22
+
23
+ - `GOOGLE_CREDENTIALS`: Google Service Account 的 JSON 憑證(字串格式)
24
+ - `SHEET_ID`: Google Sheets 的 ID
25
+ - `SHEET_RANGE`: 要讀取的範圍(預設為 "Sheet1!A:Z")
26
+
27
+ ## 使用方式
28
+
29
+ 1. 在模板區域編輯 HTML 模板,使用 `{{ 變數名 }}` 語法
30
+ 2. 在變數區域輸入 JSON 格式的變數值
31
+ 3. 歷年得獎紀錄會自動從 Google Sheets 載入
32
+ 4. 點擊「生成 HTML 檔案」按鈕下載結果
app.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request, Form
2
+ from fastapi.responses import HTMLResponse, Response
3
+ from fastapi.staticfiles import StaticFiles
4
+ from fastapi.templating import Jinja2Templates
5
+ from typing import Dict, List, Optional
6
+ import httpx
7
+ import json
8
+ from googleapiclient.discovery import build
9
+ from google.oauth2 import service_account
10
+ import os
11
+ from datetime import datetime
12
+
13
+ app = FastAPI()
14
+
15
+ templates = Jinja2Templates(directory="templates")
16
+
17
+ GOOGLE_SHEETS_SCOPES = ['https://www.googleapis.com/auth/spreadsheets.readonly']
18
+ GOOGLE_CREDENTIALS = os.getenv("GOOGLE_CREDENTIALS", "{}")
19
+ SHEET_ID = os.getenv("SHEET_ID", "")
20
+ SHEET_RANGE = os.getenv("SHEET_RANGE", "Sheet1!A:Z")
21
+
22
+ def get_sheets_data():
23
+ """Fetch data from Google Sheets"""
24
+ try:
25
+ if not GOOGLE_CREDENTIALS or GOOGLE_CREDENTIALS == "{}":
26
+ return []
27
+
28
+ creds_dict = json.loads(GOOGLE_CREDENTIALS)
29
+ creds = service_account.Credentials.from_service_account_info(
30
+ creds_dict, scopes=GOOGLE_SHEETS_SCOPES
31
+ )
32
+
33
+ service = build('sheets', 'v4', credentials=creds)
34
+ sheet = service.spreadsheets()
35
+
36
+ result = sheet.values().get(
37
+ spreadsheetId=SHEET_ID,
38
+ range=SHEET_RANGE
39
+ ).execute()
40
+
41
+ values = result.get('values', [])
42
+ if not values:
43
+ return []
44
+
45
+ headers = values[0]
46
+ data = []
47
+ for row in values[1:]:
48
+ record = {}
49
+ for i, header in enumerate(headers):
50
+ record[header] = row[i] if i < len(row) else ""
51
+ data.append(record)
52
+
53
+ return data
54
+ except Exception as e:
55
+ print(f"Error fetching Google Sheets data: {e}")
56
+ return []
57
+
58
+ @app.get("/", response_class=HTMLResponse)
59
+ async def index(request: Request):
60
+ awards_data = get_sheets_data()
61
+ return templates.TemplateResponse(
62
+ "index.html",
63
+ {
64
+ "request": request,
65
+ "awards_data": awards_data
66
+ }
67
+ )
68
+
69
+ @app.post("/generate-html")
70
+ async def generate_html(
71
+ template_content: str = Form(...),
72
+ variables: str = Form("{}")
73
+ ):
74
+ try:
75
+ variables_dict = json.loads(variables)
76
+ awards_data = get_sheets_data()
77
+
78
+ variables_dict['awards_data'] = awards_data
79
+ variables_dict['current_year'] = datetime.now().year
80
+
81
+ from jinja2 import Template
82
+ template = Template(template_content)
83
+ generated_html = template.render(**variables_dict)
84
+
85
+ return Response(
86
+ content=generated_html,
87
+ media_type="text/html",
88
+ headers={
89
+ "Content-Disposition": f"attachment; filename=generated_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html"
90
+ }
91
+ )
92
+ except Exception as e:
93
+ return {"error": str(e)}
94
+
95
+ @app.get("/api/awards")
96
+ async def get_awards():
97
+ """API endpoint to fetch awards data"""
98
+ return {"awards": get_sheets_data()}
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn[standard]
3
+ jinja2
4
+ httpx
5
+ google-auth
6
+ google-auth-oauthlib
7
+ google-auth-httplib2
8
+ google-api-python-client
9
+ python-multipart
templates/index.html ADDED
@@ -0,0 +1,259 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="zh-TW">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>龍顏文學獎 HTML 建立器</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+ body {
14
+ font-family: 'Microsoft JhengHei', Arial, sans-serif;
15
+ background-color: #f5f5f5;
16
+ padding: 20px;
17
+ }
18
+ .container {
19
+ max-width: 1400px;
20
+ margin: 0 auto;
21
+ background-color: white;
22
+ padding: 30px;
23
+ border-radius: 10px;
24
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
25
+ }
26
+ h1 {
27
+ color: #333;
28
+ margin-bottom: 30px;
29
+ text-align: center;
30
+ font-size: 2.5em;
31
+ }
32
+ .section {
33
+ margin-bottom: 30px;
34
+ padding: 20px;
35
+ background-color: #f9f9f9;
36
+ border-radius: 8px;
37
+ border: 1px solid #e0e0e0;
38
+ }
39
+ .section h2 {
40
+ color: #555;
41
+ margin-bottom: 15px;
42
+ font-size: 1.5em;
43
+ }
44
+ .template-area {
45
+ width: 100%;
46
+ min-height: 400px;
47
+ padding: 15px;
48
+ border: 1px solid #ddd;
49
+ border-radius: 5px;
50
+ font-family: 'Courier New', monospace;
51
+ font-size: 14px;
52
+ background-color: #fafafa;
53
+ resize: vertical;
54
+ }
55
+ .variables-area {
56
+ width: 100%;
57
+ min-height: 200px;
58
+ padding: 15px;
59
+ border: 1px solid #ddd;
60
+ border-radius: 5px;
61
+ font-family: 'Courier New', monospace;
62
+ font-size: 14px;
63
+ background-color: #fafafa;
64
+ resize: vertical;
65
+ }
66
+ .awards-table {
67
+ width: 100%;
68
+ border-collapse: collapse;
69
+ margin-top: 10px;
70
+ }
71
+ .awards-table th,
72
+ .awards-table td {
73
+ padding: 10px;
74
+ border: 1px solid #ddd;
75
+ text-align: left;
76
+ }
77
+ .awards-table th {
78
+ background-color: #f0f0f0;
79
+ font-weight: bold;
80
+ }
81
+ .generate-btn {
82
+ background-color: #4CAF50;
83
+ color: white;
84
+ padding: 15px 40px;
85
+ font-size: 18px;
86
+ border: none;
87
+ border-radius: 5px;
88
+ cursor: pointer;
89
+ display: block;
90
+ margin: 30px auto;
91
+ transition: background-color 0.3s;
92
+ }
93
+ .generate-btn:hover {
94
+ background-color: #45a049;
95
+ }
96
+ .info-box {
97
+ background-color: #e8f4f8;
98
+ border-left: 4px solid #2196F3;
99
+ padding: 15px;
100
+ margin-bottom: 20px;
101
+ }
102
+ .template-help {
103
+ margin-top: 10px;
104
+ padding: 10px;
105
+ background-color: #f0f8ff;
106
+ border-radius: 5px;
107
+ font-size: 0.9em;
108
+ color: #666;
109
+ }
110
+ .loading {
111
+ display: none;
112
+ text-align: center;
113
+ color: #666;
114
+ margin: 20px 0;
115
+ }
116
+ </style>
117
+ </head>
118
+ <body>
119
+ <div class="container">
120
+ <h1>龍顏文學獎 HTML 建立器</h1>
121
+
122
+ <div class="info-box">
123
+ <p><strong>使用說明:</strong></p>
124
+ <ul style="margin-left: 20px; margin-top: 10px;">
125
+ <li>在模板區域輸入 HTML 模板,使用 {{ 變數名 }} 來標記變數</li>
126
+ <li>在變數區域輸入 JSON 格式的變數值</li>
127
+ <li>歷年得獎紀錄會自動從 Google Sheets 同步</li>
128
+ <li>點擊生成按鈕即可下載完整的 HTML 檔案</li>
129
+ </ul>
130
+ </div>
131
+
132
+ <form id="htmlGeneratorForm" action="/generate-html" method="post">
133
+ <div class="section">
134
+ <h2>HTML 模板</h2>
135
+ <div class="template-help">
136
+ <strong>可用變數:</strong>
137
+ <code>{{ title }}</code>, <code>{{ subtitle }}</code>, <code>{{ content }}</code>,
138
+ <code>{{ current_year }}</code>, <code>{{ awards_data }}</code>
139
+ </div>
140
+ <textarea name="template_content" class="template-area" placeholder="輸入 HTML 模板..."><!DOCTYPE html>
141
+ <html lang="zh-TW">
142
+ <head>
143
+ <meta charset="UTF-8">
144
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
145
+ <title>{{ title }}</title>
146
+ <style>
147
+ body {
148
+ font-family: 'Microsoft JhengHei', Arial, sans-serif;
149
+ margin: 0;
150
+ padding: 0;
151
+ background-color: #f5f5f5;
152
+ }
153
+ .hero {
154
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
155
+ color: white;
156
+ padding: 60px 20px;
157
+ text-align: center;
158
+ }
159
+ .hero h1 {
160
+ font-size: 3em;
161
+ margin-bottom: 20px;
162
+ }
163
+ .content {
164
+ max-width: 1200px;
165
+ margin: 40px auto;
166
+ padding: 0 20px;
167
+ }
168
+ .awards-section {
169
+ background: white;
170
+ padding: 40px;
171
+ border-radius: 10px;
172
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
173
+ }
174
+ .award-item {
175
+ padding: 20px;
176
+ border-bottom: 1px solid #eee;
177
+ }
178
+ .award-item:last-child {
179
+ border-bottom: none;
180
+ }
181
+ </style>
182
+ </head>
183
+ <body>
184
+ <div class="hero">
185
+ <h1>{{ title }}</h1>
186
+ <p>{{ subtitle }}</p>
187
+ </div>
188
+
189
+ <div class="content">
190
+ <div class="awards-section">
191
+ <h2>歷年得獎紀錄</h2>
192
+ {% for award in awards_data %}
193
+ <div class="award-item">
194
+ <h3>{{ award.年份 }} - {{ award.獎項 }}</h3>
195
+ <p><strong>得獎者:</strong>{{ award.得獎者 }}</p>
196
+ <p><strong>作品:</strong>{{ award.作品名稱 }}</p>
197
+ </div>
198
+ {% endfor %}
199
+ </div>
200
+ </div>
201
+ </body>
202
+ </html></textarea>
203
+ </div>
204
+
205
+ <div class="section">
206
+ <h2>變數設定</h2>
207
+ <textarea name="variables" class="variables-area" placeholder='輸入 JSON 格式的變數,例如:{"title": "龍顏文學獎", "subtitle": "2024年度頒獎典禮"}'>
208
+ {
209
+ "title": "龍顏文學獎",
210
+ "subtitle": "致力於推廣優秀文學創作",
211
+ "content": "歡迎來到龍顏文學獎官方網站"
212
+ }</textarea>
213
+ </div>
214
+
215
+ <div class="section">
216
+ <h2>歷年得獎紀錄(自動同步)</h2>
217
+ <div class="loading" id="awardsLoading">載入中...</div>
218
+ {% if awards_data %}
219
+ <table class="awards-table">
220
+ <thead>
221
+ <tr>
222
+ {% for key in awards_data[0].keys() %}
223
+ <th>{{ key }}</th>
224
+ {% endfor %}
225
+ </tr>
226
+ </thead>
227
+ <tbody>
228
+ {% for award in awards_data %}
229
+ <tr>
230
+ {% for value in award.values() %}
231
+ <td>{{ value }}</td>
232
+ {% endfor %}
233
+ </tr>
234
+ {% endfor %}
235
+ </tbody>
236
+ </table>
237
+ {% else %}
238
+ <p style="color: #666;">尚未設定 Google Sheets 或無資料</p>
239
+ {% endif %}
240
+ </div>
241
+
242
+ <button type="submit" class="generate-btn">生成 HTML 檔案</button>
243
+ </form>
244
+ </div>
245
+
246
+ <script>
247
+ document.getElementById('htmlGeneratorForm').addEventListener('submit', function(e) {
248
+ const variables = document.querySelector('[name="variables"]').value;
249
+ try {
250
+ JSON.parse(variables);
251
+ } catch (error) {
252
+ e.preventDefault();
253
+ alert('變數格式錯誤!請輸入有效的 JSON 格式');
254
+ return false;
255
+ }
256
+ });
257
+ </script>
258
+ </body>
259
+ </html>