tools-template-cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (198) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +54 -0
  3. package/dist/index.js +380 -0
  4. package/dist/templates.js +44 -0
  5. package/package.json +49 -0
  6. package/supabase-expo-app/.env.example +2 -0
  7. package/supabase-expo-app/App.tsx +55 -0
  8. package/supabase-expo-app/README.md +179 -0
  9. package/supabase-expo-app/app.json +34 -0
  10. package/supabase-expo-app/assets/adaptive-icon.png +0 -0
  11. package/supabase-expo-app/assets/favicon.png +0 -0
  12. package/supabase-expo-app/assets/icon.png +0 -0
  13. package/supabase-expo-app/assets/splash-icon.png +0 -0
  14. package/supabase-expo-app/components/Account.tsx +234 -0
  15. package/supabase-expo-app/components/Auth.tsx +161 -0
  16. package/supabase-expo-app/components/Avatar.tsx +173 -0
  17. package/supabase-expo-app/index.ts +8 -0
  18. package/supabase-expo-app/lib/supabase.ts +63 -0
  19. package/supabase-expo-app/package-lock.json +8983 -0
  20. package/supabase-expo-app/package.json +32 -0
  21. package/supabase-expo-app/scripts/setup-supabase.sh +73 -0
  22. package/supabase-expo-app/supabase/schema.sql +57 -0
  23. package/supabase-expo-app/tsconfig.json +6 -0
  24. package/supabase-swiftui-app/Package.swift +32 -0
  25. package/supabase-swiftui-app/README.md +215 -0
  26. package/supabase-swiftui-app/SupabaseSwiftUIApp/AccountView.swift +181 -0
  27. package/supabase-swiftui-app/SupabaseSwiftUIApp/AuthView.swift +123 -0
  28. package/supabase-swiftui-app/SupabaseSwiftUIApp/AvatarView.swift +113 -0
  29. package/supabase-swiftui-app/SupabaseSwiftUIApp/ContentView.swift +18 -0
  30. package/supabase-swiftui-app/SupabaseSwiftUIApp/Supabase.swift +13 -0
  31. package/supabase-swiftui-app/SupabaseSwiftUIApp/SupabaseSwiftUIApp.swift +22 -0
  32. package/supabase-swiftui-app/scripts/setup-supabase.sh +67 -0
  33. package/supabase-swiftui-app/supabase/schema.sql +57 -0
  34. package/supabase-user-management/AGENTS.md +5 -0
  35. package/supabase-user-management/CLAUDE.md +1 -0
  36. package/supabase-user-management/README.md +178 -0
  37. package/supabase-user-management/app/account/account-form.tsx +174 -0
  38. package/supabase-user-management/app/account/avatar.tsx +109 -0
  39. package/supabase-user-management/app/account/page.tsx +28 -0
  40. package/supabase-user-management/app/auth/confirm/route.ts +26 -0
  41. package/supabase-user-management/app/error.tsx +14 -0
  42. package/supabase-user-management/app/favicon.ico +0 -0
  43. package/supabase-user-management/app/globals.css +130 -0
  44. package/supabase-user-management/app/layout.tsx +22 -0
  45. package/supabase-user-management/app/loading.tsx +7 -0
  46. package/supabase-user-management/app/login/actions.ts +45 -0
  47. package/supabase-user-management/app/login/page.tsx +90 -0
  48. package/supabase-user-management/app/page.tsx +16 -0
  49. package/supabase-user-management/components/ui/button.tsx +58 -0
  50. package/supabase-user-management/components.json +25 -0
  51. package/supabase-user-management/eslint.config.mjs +18 -0
  52. package/supabase-user-management/lib/supabase/client.ts +8 -0
  53. package/supabase-user-management/lib/supabase/middleware.ts +52 -0
  54. package/supabase-user-management/lib/supabase/server.ts +29 -0
  55. package/supabase-user-management/lib/utils.ts +6 -0
  56. package/supabase-user-management/next.config.ts +7 -0
  57. package/supabase-user-management/package-lock.json +9910 -0
  58. package/supabase-user-management/package.json +36 -0
  59. package/supabase-user-management/postcss.config.mjs +7 -0
  60. package/supabase-user-management/public/file.svg +1 -0
  61. package/supabase-user-management/public/globe.svg +1 -0
  62. package/supabase-user-management/public/next.svg +1 -0
  63. package/supabase-user-management/public/vercel.svg +1 -0
  64. package/supabase-user-management/public/window.svg +1 -0
  65. package/supabase-user-management/scripts/setup-supabase.sh +98 -0
  66. package/supabase-user-management/src/proxy.ts +12 -0
  67. package/supabase-user-management/supabase/schema.sql +57 -0
  68. package/supabase-user-management/tsconfig.json +34 -0
  69. package/supabase_flutter_app/.metadata +45 -0
  70. package/supabase_flutter_app/README.md +195 -0
  71. package/supabase_flutter_app/analysis_options.yaml +28 -0
  72. package/supabase_flutter_app/android/app/build.gradle.kts +44 -0
  73. package/supabase_flutter_app/android/app/src/debug/AndroidManifest.xml +7 -0
  74. package/supabase_flutter_app/android/app/src/main/AndroidManifest.xml +54 -0
  75. package/supabase_flutter_app/android/app/src/main/kotlin/com/example/supabase_flutter_app/MainActivity.kt +5 -0
  76. package/supabase_flutter_app/android/app/src/main/res/drawable/launch_background.xml +12 -0
  77. package/supabase_flutter_app/android/app/src/main/res/drawable-v21/launch_background.xml +12 -0
  78. package/supabase_flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  79. package/supabase_flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  80. package/supabase_flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  81. package/supabase_flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  82. package/supabase_flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  83. package/supabase_flutter_app/android/app/src/main/res/values/styles.xml +18 -0
  84. package/supabase_flutter_app/android/app/src/main/res/values-night/styles.xml +18 -0
  85. package/supabase_flutter_app/android/app/src/profile/AndroidManifest.xml +7 -0
  86. package/supabase_flutter_app/android/build.gradle.kts +24 -0
  87. package/supabase_flutter_app/android/gradle/wrapper/gradle-wrapper.properties +5 -0
  88. package/supabase_flutter_app/android/gradle.properties +2 -0
  89. package/supabase_flutter_app/android/settings.gradle.kts +26 -0
  90. package/supabase_flutter_app/ios/Flutter/AppFrameworkInfo.plist +26 -0
  91. package/supabase_flutter_app/ios/Flutter/Debug.xcconfig +2 -0
  92. package/supabase_flutter_app/ios/Flutter/Release.xcconfig +2 -0
  93. package/supabase_flutter_app/ios/Podfile +43 -0
  94. package/supabase_flutter_app/ios/Runner/AppDelegate.swift +13 -0
  95. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +122 -0
  96. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png +0 -0
  97. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png +0 -0
  98. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png +0 -0
  99. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png +0 -0
  100. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png +0 -0
  101. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png +0 -0
  102. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png +0 -0
  103. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png +0 -0
  104. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png +0 -0
  105. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png +0 -0
  106. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png +0 -0
  107. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png +0 -0
  108. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png +0 -0
  109. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png +0 -0
  110. package/supabase_flutter_app/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png +0 -0
  111. package/supabase_flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +23 -0
  112. package/supabase_flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png +0 -0
  113. package/supabase_flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png +0 -0
  114. package/supabase_flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png +0 -0
  115. package/supabase_flutter_app/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +5 -0
  116. package/supabase_flutter_app/ios/Runner/Base.lproj/LaunchScreen.storyboard +37 -0
  117. package/supabase_flutter_app/ios/Runner/Base.lproj/Main.storyboard +26 -0
  118. package/supabase_flutter_app/ios/Runner/Info.plist +61 -0
  119. package/supabase_flutter_app/ios/Runner/Runner-Bridging-Header.h +1 -0
  120. package/supabase_flutter_app/ios/Runner.xcodeproj/project.pbxproj +619 -0
  121. package/supabase_flutter_app/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +7 -0
  122. package/supabase_flutter_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  123. package/supabase_flutter_app/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  124. package/supabase_flutter_app/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +101 -0
  125. package/supabase_flutter_app/ios/Runner.xcworkspace/contents.xcworkspacedata +7 -0
  126. package/supabase_flutter_app/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  127. package/supabase_flutter_app/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +8 -0
  128. package/supabase_flutter_app/ios/RunnerTests/RunnerTests.swift +12 -0
  129. package/supabase_flutter_app/lib/components/avatar.dart +153 -0
  130. package/supabase_flutter_app/lib/main.dart +70 -0
  131. package/supabase_flutter_app/lib/pages/account_page.dart +189 -0
  132. package/supabase_flutter_app/lib/pages/login_page.dart +150 -0
  133. package/supabase_flutter_app/linux/CMakeLists.txt +128 -0
  134. package/supabase_flutter_app/linux/flutter/CMakeLists.txt +88 -0
  135. package/supabase_flutter_app/linux/flutter/generated_plugin_registrant.cc +23 -0
  136. package/supabase_flutter_app/linux/flutter/generated_plugin_registrant.h +15 -0
  137. package/supabase_flutter_app/linux/flutter/generated_plugins.cmake +26 -0
  138. package/supabase_flutter_app/linux/runner/CMakeLists.txt +26 -0
  139. package/supabase_flutter_app/linux/runner/main.cc +6 -0
  140. package/supabase_flutter_app/linux/runner/my_application.cc +148 -0
  141. package/supabase_flutter_app/linux/runner/my_application.h +21 -0
  142. package/supabase_flutter_app/macos/Flutter/Flutter-Debug.xcconfig +2 -0
  143. package/supabase_flutter_app/macos/Flutter/Flutter-Release.xcconfig +2 -0
  144. package/supabase_flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +18 -0
  145. package/supabase_flutter_app/macos/Podfile +42 -0
  146. package/supabase_flutter_app/macos/Runner/AppDelegate.swift +13 -0
  147. package/supabase_flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +68 -0
  148. package/supabase_flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  149. package/supabase_flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  150. package/supabase_flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  151. package/supabase_flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  152. package/supabase_flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  153. package/supabase_flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  154. package/supabase_flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  155. package/supabase_flutter_app/macos/Runner/Base.lproj/MainMenu.xib +343 -0
  156. package/supabase_flutter_app/macos/Runner/Configs/AppInfo.xcconfig +14 -0
  157. package/supabase_flutter_app/macos/Runner/Configs/Debug.xcconfig +2 -0
  158. package/supabase_flutter_app/macos/Runner/Configs/Release.xcconfig +2 -0
  159. package/supabase_flutter_app/macos/Runner/Configs/Warnings.xcconfig +13 -0
  160. package/supabase_flutter_app/macos/Runner/DebugProfile.entitlements +12 -0
  161. package/supabase_flutter_app/macos/Runner/Info.plist +32 -0
  162. package/supabase_flutter_app/macos/Runner/MainFlutterWindow.swift +15 -0
  163. package/supabase_flutter_app/macos/Runner/Release.entitlements +8 -0
  164. package/supabase_flutter_app/macos/Runner.xcodeproj/project.pbxproj +705 -0
  165. package/supabase_flutter_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  166. package/supabase_flutter_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +99 -0
  167. package/supabase_flutter_app/macos/Runner.xcworkspace/contents.xcworkspacedata +7 -0
  168. package/supabase_flutter_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +8 -0
  169. package/supabase_flutter_app/macos/RunnerTests/RunnerTests.swift +12 -0
  170. package/supabase_flutter_app/pubspec.lock +818 -0
  171. package/supabase_flutter_app/pubspec.yaml +26 -0
  172. package/supabase_flutter_app/scripts/setup-supabase.sh +72 -0
  173. package/supabase_flutter_app/supabase/schema.sql +57 -0
  174. package/supabase_flutter_app/test/widget_test.dart +30 -0
  175. package/supabase_flutter_app/web/favicon.png +0 -0
  176. package/supabase_flutter_app/web/icons/Icon-192.png +0 -0
  177. package/supabase_flutter_app/web/icons/Icon-512.png +0 -0
  178. package/supabase_flutter_app/web/icons/Icon-maskable-192.png +0 -0
  179. package/supabase_flutter_app/web/icons/Icon-maskable-512.png +0 -0
  180. package/supabase_flutter_app/web/index.html +38 -0
  181. package/supabase_flutter_app/web/manifest.json +35 -0
  182. package/supabase_flutter_app/windows/CMakeLists.txt +108 -0
  183. package/supabase_flutter_app/windows/flutter/CMakeLists.txt +109 -0
  184. package/supabase_flutter_app/windows/flutter/generated_plugin_registrant.cc +20 -0
  185. package/supabase_flutter_app/windows/flutter/generated_plugin_registrant.h +15 -0
  186. package/supabase_flutter_app/windows/flutter/generated_plugins.cmake +26 -0
  187. package/supabase_flutter_app/windows/runner/CMakeLists.txt +40 -0
  188. package/supabase_flutter_app/windows/runner/Runner.rc +121 -0
  189. package/supabase_flutter_app/windows/runner/flutter_window.cpp +71 -0
  190. package/supabase_flutter_app/windows/runner/flutter_window.h +33 -0
  191. package/supabase_flutter_app/windows/runner/main.cpp +43 -0
  192. package/supabase_flutter_app/windows/runner/resource.h +16 -0
  193. package/supabase_flutter_app/windows/runner/resources/app_icon.ico +0 -0
  194. package/supabase_flutter_app/windows/runner/runner.exe.manifest +14 -0
  195. package/supabase_flutter_app/windows/runner/utils.cpp +65 -0
  196. package/supabase_flutter_app/windows/runner/utils.h +19 -0
  197. package/supabase_flutter_app/windows/runner/win32_window.cpp +288 -0
  198. package/supabase_flutter_app/windows/runner/win32_window.h +102 -0
@@ -0,0 +1,174 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useEffect, useState } from 'react'
4
+ import { createClient } from '@/lib/supabase/client'
5
+ import { type User } from '@supabase/supabase-js'
6
+ import { useRouter } from 'next/navigation'
7
+ import Avatar from './avatar'
8
+
9
+ interface Profile {
10
+ username: string
11
+ full_name: string
12
+ website: string
13
+ avatar_url: string
14
+ }
15
+
16
+ export default function AccountForm({ user }: { user: User | null }) {
17
+ const supabase = createClient()
18
+ const router = useRouter()
19
+ const [loading, setLoading] = useState(true)
20
+ const [profile, setProfile] = useState<Profile>({
21
+ username: '',
22
+ full_name: '',
23
+ website: '',
24
+ avatar_url: '',
25
+ })
26
+
27
+ const getProfile = useCallback(async () => {
28
+ try {
29
+ setLoading(true)
30
+
31
+ const { data, error, status } = await supabase
32
+ .from('profiles')
33
+ .select(`username, full_name, website, avatar_url`)
34
+ .eq('id', user?.id)
35
+ .single()
36
+
37
+ if (error && status !== 406) {
38
+ throw error
39
+ }
40
+
41
+ if (data) {
42
+ setProfile({
43
+ username: data.username || '',
44
+ full_name: data.full_name || '',
45
+ website: data.website || '',
46
+ avatar_url: data.avatar_url || '',
47
+ })
48
+ }
49
+ } catch (error) {
50
+ alert('获取资料失败!')
51
+ } finally {
52
+ setLoading(false)
53
+ }
54
+ }, [user, supabase])
55
+
56
+ useEffect(() => {
57
+ getProfile()
58
+ }, [user, getProfile])
59
+
60
+ async function updateProfile({
61
+ username,
62
+ website,
63
+ avatar_url,
64
+ }: {
65
+ username: string
66
+ website: string
67
+ avatar_url: string
68
+ }) {
69
+ try {
70
+ setLoading(true)
71
+
72
+ const { error } = await supabase.from('profiles').upsert({
73
+ id: user?.id as string,
74
+ full_name: profile.full_name,
75
+ username,
76
+ website,
77
+ avatar_url,
78
+ updated_at: new Date().toISOString(),
79
+ })
80
+
81
+ if (error) throw error
82
+ alert('资料已更新!')
83
+ } catch (error) {
84
+ alert('更新资料失败!')
85
+ } finally {
86
+ setLoading(false)
87
+ }
88
+ }
89
+
90
+ const handleSignOut = async () => {
91
+ await supabase.auth.signOut()
92
+ router.push('/login')
93
+ }
94
+
95
+ return (
96
+ <div className="space-y-8">
97
+ <div className="space-y-2">
98
+ <label className="text-sm font-medium">邮箱</label>
99
+ <input
100
+ type="text"
101
+ value={user?.email}
102
+ disabled
103
+ className="w-full rounded-md border border-input bg-muted px-3 py-2 text-sm"
104
+ />
105
+ </div>
106
+
107
+ <Avatar
108
+ uid={user?.id ?? null}
109
+ url={profile.avatar_url}
110
+ size={150}
111
+ onUpload={(url) => {
112
+ setProfile({ ...profile, avatar_url: url })
113
+ updateProfile({ ...profile, avatar_url: url })
114
+ }}
115
+ />
116
+
117
+ <div className="space-y-2">
118
+ <label htmlFor="fullName" className="text-sm font-medium">
119
+ 姓名
120
+ </label>
121
+ <input
122
+ id="fullName"
123
+ type="text"
124
+ value={profile.full_name || ''}
125
+ onChange={(e) => setProfile({ ...profile, full_name: e.target.value })}
126
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
127
+ />
128
+ </div>
129
+
130
+ <div className="space-y-2">
131
+ <label htmlFor="username" className="text-sm font-medium">
132
+ 用户名
133
+ </label>
134
+ <input
135
+ id="username"
136
+ type="text"
137
+ value={profile.username || ''}
138
+ onChange={(e) => setProfile({ ...profile, username: e.target.value })}
139
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
140
+ />
141
+ </div>
142
+
143
+ <div className="space-y-2">
144
+ <label htmlFor="website" className="text-sm font-medium">
145
+ 网站
146
+ </label>
147
+ <input
148
+ id="website"
149
+ type="url"
150
+ value={profile.website || ''}
151
+ onChange={(e) => setProfile({ ...profile, website: e.target.value })}
152
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
153
+ />
154
+ </div>
155
+
156
+ <div className="flex gap-3">
157
+ <button
158
+ onClick={() => updateProfile(profile)}
159
+ disabled={loading}
160
+ className="flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
161
+ >
162
+ {loading ? '加载中...' : '更新资料'}
163
+ </button>
164
+
165
+ <button
166
+ onClick={handleSignOut}
167
+ className="rounded-md border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent"
168
+ >
169
+ 退出登录
170
+ </button>
171
+ </div>
172
+ </div>
173
+ )
174
+ }
@@ -0,0 +1,109 @@
1
+ 'use client'
2
+
3
+ import { useCallback, useState } from 'react'
4
+ import { createClient } from '@/lib/supabase/client'
5
+ import Image from 'next/image'
6
+
7
+ interface AvatarProps {
8
+ uid: string | null
9
+ url: string | null
10
+ size: number
11
+ onUpload: (url: string) => void
12
+ }
13
+
14
+ export default function Avatar({ uid, url, size, onUpload }: AvatarProps) {
15
+ const supabase = createClient()
16
+ const [uploading, setUploading] = useState(false)
17
+
18
+ const uploadAvatar = useCallback(
19
+ async (event: React.ChangeEvent<HTMLInputElement>) => {
20
+ try {
21
+ setUploading(true)
22
+
23
+ if (!event.target.files || event.target.files.length === 0) {
24
+ throw new Error('请选择要上传的图片。')
25
+ }
26
+
27
+ const file = event.target.files[0]
28
+ const fileExt = file.name.split('.').pop()
29
+ const filePath = `${uid}-${Math.random()}.${fileExt}`
30
+
31
+ const { error: uploadError } = await supabase.storage
32
+ .from('avatars')
33
+ .upload(filePath, file)
34
+
35
+ if (uploadError) {
36
+ throw uploadError
37
+ }
38
+
39
+ onUpload(filePath)
40
+ } catch (error) {
41
+ alert('上传头像失败!')
42
+ } finally {
43
+ setUploading(false)
44
+ }
45
+ },
46
+ [uid, supabase, onUpload]
47
+ )
48
+
49
+ const downloadImage = useCallback(async () => {
50
+ try {
51
+ const { data, error } = await supabase.storage.from('avatars').download(url!)
52
+ if (error) {
53
+ throw error
54
+ }
55
+ return URL.createObjectURL(data)
56
+ } catch {
57
+ return null
58
+ }
59
+ }, [url, supabase])
60
+
61
+ const [avatarUrl, setAvatarUrl] = useState<string | null>(null)
62
+
63
+ useCallback(() => {
64
+ if (url) {
65
+ downloadImage().then(setAvatarUrl)
66
+ }
67
+ }, [url, downloadImage])
68
+
69
+ return (
70
+ <div className="space-y-2">
71
+ <label className="text-sm font-medium">头像</label>
72
+ <div className="flex items-center gap-4">
73
+ {url ? (
74
+ <Image
75
+ src={avatarUrl || ''}
76
+ alt="Avatar"
77
+ className="rounded-full border"
78
+ width={size}
79
+ height={size}
80
+ style={{ height: size, width: size }}
81
+ />
82
+ ) : (
83
+ <div
84
+ className="rounded-full border bg-muted flex items-center justify-center"
85
+ style={{ height: size, width: size }}
86
+ >
87
+ <span className="text-muted-foreground">无头像</span>
88
+ </div>
89
+ )}
90
+ <div>
91
+ <label
92
+ htmlFor="avatar-upload"
93
+ className="cursor-pointer rounded-md border border-input bg-background px-3 py-2 text-sm hover:bg-accent"
94
+ >
95
+ {uploading ? '上传中...' : '上传'}
96
+ </label>
97
+ <input
98
+ id="avatar-upload"
99
+ type="file"
100
+ accept="image/*"
101
+ onChange={uploadAvatar}
102
+ disabled={uploading}
103
+ className="hidden"
104
+ />
105
+ </div>
106
+ </div>
107
+ </div>
108
+ )
109
+ }
@@ -0,0 +1,28 @@
1
+ import { createClient } from '@/lib/supabase/server'
2
+ import { redirect } from 'next/navigation'
3
+ import AccountForm from './account-form'
4
+
5
+ export default async function AccountPage() {
6
+ const supabase = await createClient()
7
+
8
+ const {
9
+ data: { user },
10
+ } = await supabase.auth.getUser()
11
+
12
+ if (!user) {
13
+ return redirect('/login')
14
+ }
15
+
16
+ return (
17
+ <div className="flex min-h-screen flex-col items-center justify-center px-4 py-8">
18
+ <div className="w-full max-w-md space-y-8">
19
+ <div className="text-center">
20
+ <h1 className="text-3xl font-bold">账户设置</h1>
21
+ <p className="mt-2 text-muted-foreground">管理您的个人资料</p>
22
+ </div>
23
+
24
+ <AccountForm user={user} />
25
+ </div>
26
+ </div>
27
+ )
28
+ }
@@ -0,0 +1,26 @@
1
+ import { createClient } from '@/lib/supabase/server'
2
+ import { type EmailOtpType } from '@supabase/supabase-js'
3
+ import { redirect } from 'next/navigation'
4
+ import { type NextRequest } from 'next/server'
5
+
6
+ export async function GET(request: NextRequest) {
7
+ const { searchParams } = new URL(request.url)
8
+ const token_hash = searchParams.get('token_hash')
9
+ const type = searchParams.get('type') as EmailOtpType | null
10
+ const next = searchParams.get('next') ?? '/account'
11
+
12
+ if (token_hash && type) {
13
+ const supabase = await createClient()
14
+
15
+ const { error } = await supabase.auth.verifyOtp({
16
+ type,
17
+ token_hash,
18
+ })
19
+
20
+ if (!error) {
21
+ redirect(next)
22
+ }
23
+ }
24
+
25
+ redirect('/login?message=验证失败,请重试')
26
+ }
@@ -0,0 +1,14 @@
1
+ 'use client'
2
+
3
+ export default function Error({
4
+ error,
5
+ }: {
6
+ error: Error & { digest?: string }
7
+ }) {
8
+ return (
9
+ <div className="flex flex-col items-center justify-center min-h-screen">
10
+ <h2 className="text-2xl font-semibold mb-4">出错了!</h2>
11
+ <p className="text-muted-foreground">{error.message}</p>
12
+ </div>
13
+ )
14
+ }
@@ -0,0 +1,130 @@
1
+ @import "tailwindcss";
2
+ @import "tw-animate-css";
3
+ @import "shadcn/tailwind.css";
4
+
5
+ @custom-variant dark (&:is(.dark *));
6
+
7
+ @theme inline {
8
+ --color-background: var(--background);
9
+ --color-foreground: var(--foreground);
10
+ --font-sans: var(--font-sans);
11
+ --font-mono: var(--font-geist-mono);
12
+ --font-heading: var(--font-sans);
13
+ --color-sidebar-ring: var(--sidebar-ring);
14
+ --color-sidebar-border: var(--sidebar-border);
15
+ --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
16
+ --color-sidebar-accent: var(--sidebar-accent);
17
+ --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
18
+ --color-sidebar-primary: var(--sidebar-primary);
19
+ --color-sidebar-foreground: var(--sidebar-foreground);
20
+ --color-sidebar: var(--sidebar);
21
+ --color-chart-5: var(--chart-5);
22
+ --color-chart-4: var(--chart-4);
23
+ --color-chart-3: var(--chart-3);
24
+ --color-chart-2: var(--chart-2);
25
+ --color-chart-1: var(--chart-1);
26
+ --color-ring: var(--ring);
27
+ --color-input: var(--input);
28
+ --color-border: var(--border);
29
+ --color-destructive: var(--destructive);
30
+ --color-accent-foreground: var(--accent-foreground);
31
+ --color-accent: var(--accent);
32
+ --color-muted-foreground: var(--muted-foreground);
33
+ --color-muted: var(--muted);
34
+ --color-secondary-foreground: var(--secondary-foreground);
35
+ --color-secondary: var(--secondary);
36
+ --color-primary-foreground: var(--primary-foreground);
37
+ --color-primary: var(--primary);
38
+ --color-popover-foreground: var(--popover-foreground);
39
+ --color-popover: var(--popover);
40
+ --color-card-foreground: var(--card-foreground);
41
+ --color-card: var(--card);
42
+ --radius-sm: calc(var(--radius) * 0.6);
43
+ --radius-md: calc(var(--radius) * 0.8);
44
+ --radius-lg: var(--radius);
45
+ --radius-xl: calc(var(--radius) * 1.4);
46
+ --radius-2xl: calc(var(--radius) * 1.8);
47
+ --radius-3xl: calc(var(--radius) * 2.2);
48
+ --radius-4xl: calc(var(--radius) * 2.6);
49
+ }
50
+
51
+ :root {
52
+ --background: oklch(1 0 0);
53
+ --foreground: oklch(0.145 0 0);
54
+ --card: oklch(1 0 0);
55
+ --card-foreground: oklch(0.145 0 0);
56
+ --popover: oklch(1 0 0);
57
+ --popover-foreground: oklch(0.145 0 0);
58
+ --primary: oklch(0.205 0 0);
59
+ --primary-foreground: oklch(0.985 0 0);
60
+ --secondary: oklch(0.97 0 0);
61
+ --secondary-foreground: oklch(0.205 0 0);
62
+ --muted: oklch(0.97 0 0);
63
+ --muted-foreground: oklch(0.556 0 0);
64
+ --accent: oklch(0.97 0 0);
65
+ --accent-foreground: oklch(0.205 0 0);
66
+ --destructive: oklch(0.577 0.245 27.325);
67
+ --border: oklch(0.922 0 0);
68
+ --input: oklch(0.922 0 0);
69
+ --ring: oklch(0.708 0 0);
70
+ --chart-1: oklch(0.87 0 0);
71
+ --chart-2: oklch(0.556 0 0);
72
+ --chart-3: oklch(0.439 0 0);
73
+ --chart-4: oklch(0.371 0 0);
74
+ --chart-5: oklch(0.269 0 0);
75
+ --radius: 0.625rem;
76
+ --sidebar: oklch(0.985 0 0);
77
+ --sidebar-foreground: oklch(0.145 0 0);
78
+ --sidebar-primary: oklch(0.205 0 0);
79
+ --sidebar-primary-foreground: oklch(0.985 0 0);
80
+ --sidebar-accent: oklch(0.97 0 0);
81
+ --sidebar-accent-foreground: oklch(0.205 0 0);
82
+ --sidebar-border: oklch(0.922 0 0);
83
+ --sidebar-ring: oklch(0.708 0 0);
84
+ }
85
+
86
+ .dark {
87
+ --background: oklch(0.145 0 0);
88
+ --foreground: oklch(0.985 0 0);
89
+ --card: oklch(0.205 0 0);
90
+ --card-foreground: oklch(0.985 0 0);
91
+ --popover: oklch(0.205 0 0);
92
+ --popover-foreground: oklch(0.985 0 0);
93
+ --primary: oklch(0.922 0 0);
94
+ --primary-foreground: oklch(0.205 0 0);
95
+ --secondary: oklch(0.269 0 0);
96
+ --secondary-foreground: oklch(0.985 0 0);
97
+ --muted: oklch(0.269 0 0);
98
+ --muted-foreground: oklch(0.708 0 0);
99
+ --accent: oklch(0.269 0 0);
100
+ --accent-foreground: oklch(0.985 0 0);
101
+ --destructive: oklch(0.704 0.191 22.216);
102
+ --border: oklch(1 0 0 / 10%);
103
+ --input: oklch(1 0 0 / 15%);
104
+ --ring: oklch(0.556 0 0);
105
+ --chart-1: oklch(0.87 0 0);
106
+ --chart-2: oklch(0.556 0 0);
107
+ --chart-3: oklch(0.439 0 0);
108
+ --chart-4: oklch(0.371 0 0);
109
+ --chart-5: oklch(0.269 0 0);
110
+ --sidebar: oklch(0.205 0 0);
111
+ --sidebar-foreground: oklch(0.985 0 0);
112
+ --sidebar-primary: oklch(0.488 0.243 264.376);
113
+ --sidebar-primary-foreground: oklch(0.985 0 0);
114
+ --sidebar-accent: oklch(0.269 0 0);
115
+ --sidebar-accent-foreground: oklch(0.985 0 0);
116
+ --sidebar-border: oklch(1 0 0 / 10%);
117
+ --sidebar-ring: oklch(0.556 0 0);
118
+ }
119
+
120
+ @layer base {
121
+ * {
122
+ @apply border-border outline-ring/50;
123
+ }
124
+ body {
125
+ @apply bg-background text-foreground;
126
+ }
127
+ html {
128
+ @apply font-sans;
129
+ }
130
+ }
@@ -0,0 +1,22 @@
1
+ import type { Metadata } from 'next'
2
+ import { Inter } from 'next/font/google'
3
+ import './globals.css'
4
+
5
+ const inter = Inter({ subsets: ['latin'] })
6
+
7
+ export const metadata: Metadata = {
8
+ title: 'Supabase User Management',
9
+ description: '使用 Next.js 和 Supabase 构建的用户管理应用',
10
+ }
11
+
12
+ export default function RootLayout({
13
+ children,
14
+ }: {
15
+ children: React.ReactNode
16
+ }) {
17
+ return (
18
+ <html lang="zh-CN">
19
+ <body className={inter.className}>{children}</body>
20
+ </html>
21
+ )
22
+ }
@@ -0,0 +1,7 @@
1
+ export default function Loading() {
2
+ return (
3
+ <div className="flex items-center justify-center min-h-screen">
4
+ <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
5
+ </div>
6
+ )
7
+ }
@@ -0,0 +1,45 @@
1
+ 'use server'
2
+
3
+ import { createClient } from '@/lib/supabase/server'
4
+ import { headers } from 'next/headers'
5
+ import { redirect } from 'next/navigation'
6
+
7
+ export async function signUp(formData: FormData) {
8
+ const supabase = await createClient()
9
+ const origin = (await headers()).get('origin')
10
+
11
+ const { error } = await supabase.auth.signUp({
12
+ email: formData.get('email') as string,
13
+ password: formData.get('password') as string,
14
+ options: {
15
+ emailRedirectTo: `${origin}/auth/confirm`,
16
+ },
17
+ })
18
+
19
+ if (error) {
20
+ return redirect('/login?message=' + encodeURIComponent(error.message))
21
+ }
22
+
23
+ return redirect('/login?message=检查邮箱以确认注册')
24
+ }
25
+
26
+ export async function signIn(formData: FormData) {
27
+ const supabase = await createClient()
28
+
29
+ const { error } = await supabase.auth.signInWithPassword({
30
+ email: formData.get('email') as string,
31
+ password: formData.get('password') as string,
32
+ })
33
+
34
+ if (error) {
35
+ return redirect('/login?message=' + encodeURIComponent(error.message))
36
+ }
37
+
38
+ return redirect('/account')
39
+ }
40
+
41
+ export async function signOut() {
42
+ const supabase = await createClient()
43
+ await supabase.auth.signOut()
44
+ return redirect('/login')
45
+ }
@@ -0,0 +1,90 @@
1
+ import { createClient } from '@/lib/supabase/server'
2
+ import { redirect } from 'next/navigation'
3
+ import Link from 'next/link'
4
+ import { signIn, signUp } from './actions'
5
+
6
+ export default async function LoginPage({
7
+ searchParams,
8
+ }: {
9
+ searchParams: { message?: string }
10
+ }) {
11
+ const supabase = await createClient()
12
+
13
+ const {
14
+ data: { user },
15
+ } = await supabase.auth.getUser()
16
+
17
+ if (user) {
18
+ return redirect('/account')
19
+ }
20
+
21
+ return (
22
+ <div className="flex min-h-screen flex-col items-center justify-center px-4 py-8">
23
+ <div className="w-full max-w-md space-y-8">
24
+ <div className="text-center">
25
+ <h1 className="text-3xl font-bold">用户管理</h1>
26
+ <p className="mt-2 text-muted-foreground">登录或注册账户</p>
27
+ </div>
28
+
29
+ {searchParams.message && (
30
+ <div className="rounded-md bg-primary/10 p-4 text-center text-sm">
31
+ {searchParams.message}
32
+ </div>
33
+ )}
34
+
35
+ <div className="space-y-4">
36
+ <form className="space-y-4">
37
+ <div className="space-y-2">
38
+ <label htmlFor="email" className="text-sm font-medium">
39
+ 邮箱
40
+ </label>
41
+ <input
42
+ id="email"
43
+ name="email"
44
+ type="email"
45
+ required
46
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
47
+ placeholder="your@email.com"
48
+ />
49
+ </div>
50
+
51
+ <div className="space-y-2">
52
+ <label htmlFor="password" className="text-sm font-medium">
53
+ 密码
54
+ </label>
55
+ <input
56
+ id="password"
57
+ name="password"
58
+ type="password"
59
+ required
60
+ className="w-full rounded-md border border-input bg-background px-3 py-2 text-sm"
61
+ placeholder="••••••••"
62
+ />
63
+ </div>
64
+
65
+ <div className="flex gap-3">
66
+ <button
67
+ formAction={signIn}
68
+ className="flex-1 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
69
+ >
70
+ 登录
71
+ </button>
72
+ <button
73
+ formAction={signUp}
74
+ className="flex-1 rounded-md border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent"
75
+ >
76
+ 注册
77
+ </button>
78
+ </div>
79
+ </form>
80
+ </div>
81
+
82
+ <p className="text-center text-sm text-muted-foreground">
83
+ <Link href="/" className="text-primary hover:underline">
84
+ 返回首页
85
+ </Link>
86
+ </p>
87
+ </div>
88
+ </div>
89
+ )
90
+ }
@@ -0,0 +1,16 @@
1
+ import { createClient } from '@/lib/supabase/server'
2
+ import { redirect } from 'next/navigation'
3
+
4
+ export default async function HomePage() {
5
+ const supabase = await createClient()
6
+
7
+ const {
8
+ data: { user },
9
+ } = await supabase.auth.getUser()
10
+
11
+ if (user) {
12
+ return redirect('/account')
13
+ }
14
+
15
+ return redirect('/login')
16
+ }