├── .gitignore
├── pb_data
├── data.db
├── auxiliary.db
└── storage
│ └── _pb_users_auth_
│ └── s9du2le16jk23ju
│ ├── acg8oc_lkpm_we_szj_xp_q_o4_yi_qz_p8_v9_ak2sjcz5_tjn_b0dhg0_aj7_p_tcd_z2_s96_c_02l1wdhu87_whhbcky808.png
│ ├── acg8oc_lkpm_we_szj_xp_q_o4_yi_qz_p8_v9_ak2sjcz5_tjn_b0dhg0_aj7_p_tcd_z2_s96_c_02l1wdhu87_whhbcky808.png.attrs
│ └── thumbs_acg8oc_lkpm_we_szj_xp_q_o4_yi_qz_p8_v9_ak2sjcz5_tjn_b0dhg0_aj7_p_tcd_z2_s96_c_02l1wdhu87_whhbcky808.png
│ ├── 100x100_acg8oc_lkpm_we_szj_xp_q_o4_yi_qz_p8_v9_ak2sjcz5_tjn_b0dhg0_aj7_p_tcd_z2_s96_c_02l1wdhu87_whhbcky808.png.attrs
│ └── 100x100_acg8oc_lkpm_we_szj_xp_q_o4_yi_qz_p8_v9_ak2sjcz5_tjn_b0dhg0_aj7_p_tcd_z2_s96_c_02l1wdhu87_whhbcky808.png
├── static
├── .DS_Store
├── assets
│ ├── favicon.ico
│ ├── favicon-96x96.png
│ ├── apple-touch-icon.png
│ ├── peng-background.webp
│ ├── web-app-manifest-192x192.png
│ ├── web-app-manifest-512x512.png
│ └── site.webmanifest
└── css
│ └── input.css
├── views
├── components
│ ├── RefreshOnContent.templ
│ ├── RefreshOnContent_templ.go
│ ├── Alert.templ
│ ├── Background.templ
│ ├── ChatBubble.templ
│ ├── Background_templ.go
│ ├── Alert_templ.go
│ ├── Logos.templ
│ ├── ChatBubble_templ.go
│ ├── Icons.templ
│ └── Logos_templ.go
├── layouts
│ ├── MainLayout.templ
│ └── MainLayout_templ.go
└── pages
│ ├── AuthRedirect.templ
│ ├── LoginPage.templ
│ ├── AuthRedirect_templ.go
│ ├── LoginPage_templ.go
│ └── HomePage.templ
├── cmd
├── db-server
│ └── main.go
└── t3-clone
│ └── main.go
├── Makefile
├── handlers
├── hotreload.go
├── pinghub.go
├── ssehub.go
├── home.go
├── static.go
├── login.go
├── auth.go
└── chat.go
├── db
├── db.go
├── authMethods.go
├── models.go
└── auth.go
├── LICENSE
├── services
├── ssehub.go
├── sessions.go
└── openrouter.go
├── go.mod
├── README.md
├── utils
└── loggger.go
└── pb_schema.json
/.gitignore:
--------------------------------------------------------------------------------
1 | go.sum
2 | .env
3 |
4 | tailwindcss
5 | pocketbase
6 |
--------------------------------------------------------------------------------
/pb_data/data.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/morethancoder/t3-clone/main/pb_data/data.db
--------------------------------------------------------------------------------
/static/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/morethancoder/t3-clone/main/static/.DS_Store
--------------------------------------------------------------------------------
/pb_data/auxiliary.db:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/morethancoder/t3-clone/main/pb_data/auxiliary.db
--------------------------------------------------------------------------------
/static/assets/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/morethancoder/t3-clone/main/static/assets/favicon.ico
--------------------------------------------------------------------------------
/static/assets/favicon-96x96.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/morethancoder/t3-clone/main/static/assets/favicon-96x96.png
--------------------------------------------------------------------------------
/static/assets/apple-touch-icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/morethancoder/t3-clone/main/static/assets/apple-touch-icon.png
--------------------------------------------------------------------------------
/static/assets/peng-background.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/morethancoder/t3-clone/main/static/assets/peng-background.webp
--------------------------------------------------------------------------------
/static/assets/web-app-manifest-192x192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/morethancoder/t3-clone/main/static/assets/web-app-manifest-192x192.png
--------------------------------------------------------------------------------
/static/assets/web-app-manifest-512x512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/morethancoder/t3-clone/main/static/assets/web-app-manifest-512x512.png
--------------------------------------------------------------------------------
/views/components/RefreshOnContent.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | templ RefreshOnContent() {
4 |
22 | cd gunter
23 | ```
24 |
25 | 2. **Install Dependencies**
26 | Run the following command to install all required dependencies:
27 | ```bash
28 | go mod tidy
29 | ```
30 |
31 | 3. **Set Up PocketBase Schema**
32 | - Open your PocketBase admin panel.
33 | - Load the schema from the `pb_schema.json` file into PocketBase.
34 | (This file contains the necessary collections and fields for the project.)
35 |
36 | 4. **Run PocketBase**
37 | You can start the PocketBase server using the provided `Makefile`. Simply run:
38 | ```bash
39 | make db
40 | ```
41 |
42 | 5. **Configure Google OAuth**
43 | - Add your Google OAuth provider details (Client ID and Client Secret) in the PocketBase admin panel under the "Auth Providers" section.
44 |
45 | ---
46 |
47 | ### Running the Project
48 |
49 | Once the setup is complete, everything should work as expected. Start your PocketBase server using `make db` and run the project.
50 |
51 | ---
52 |
53 | ## Notes
54 |
55 | - Ensure that your PocketBase server is running before interacting with the project.
56 | - If you encounter any issues, double-check the schema and OAuth provider configuration.
57 |
58 | ---
59 |
60 | Enjoy building with **Gunter**! 🎉
61 |
62 |
63 | ## Docker
64 |
65 | ### How to Use
66 | 1. Build the Docker Image
67 | ```bash
68 | cd /path/to/your/t3-clone
69 | docker build -t morethancoder/t3-clone .
70 | ```
71 | 2. Run with Default Settings
72 | ```bash
73 | docker run -p 8080:8080 -p 8090:8090 morethancoder/t3-clone
74 | ```
75 | 3. Run with Custom Environment Variables
76 | ```bash
77 | docker run -p 8080:8080 -p 8090:8090 -e OPENROUTER_API_KEY=your_actual_api_key -e ENV=production morethancoder/t3-clone
78 | ```
79 | 4. Run with Persistent Database
80 | ```bash
81 | docker run -p 8080:8080 -p 8090:8090 -v $(pwd)/pb_data:/root/pb_data -e OPENROUTER_API_KEY=your_actual_api_key morethancoder/t3-clone
82 | ```
83 | 5. Run in Background (Detached Mode)
84 | ```bash
85 | docker run -d -p 8080:8080 -p 8090:8090 --name t3-clone-app -e OPENROUTER_API_KEY=your_actual_api_key morethancoder/t3-clone
86 | ```
87 |
88 | #### Access Your Application
89 | ```
90 |
91 | Main app: http://localhost:8080
92 | Database server: http://localhost:8090
93 | ```
94 | #### Stop the Container
95 | ```bash
96 | docker stop t3-clone-app
97 | docker rm t3-clone-app
98 | ```
99 |
--------------------------------------------------------------------------------
/views/components/Background.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 |
4 | templ BackgroundBlobs() {
5 |
67 |
73 | }
74 |
75 |
--------------------------------------------------------------------------------
/utils/loggger.go:
--------------------------------------------------------------------------------
1 | package utils
2 |
3 | import (
4 | "fmt"
5 | "io"
6 | "os"
7 | "sync"
8 | "time"
9 | )
10 |
11 | type Logger interface {
12 | Info(format string, args ...any)
13 | Success(format string, args ...any)
14 | Error(format string, args ...any)
15 | Loading(format string, args ...any)
16 | Warn(format string, args ...any)
17 | Debug(format string, args ...any)
18 | Fatal(format string, args ...any)
19 | }
20 |
21 | type logMode string
22 |
23 | const (
24 | infoMode logMode = "INFO"
25 | successMode logMode = "SUCCESS"
26 | errorMode logMode = "ERROR"
27 | loadingMode logMode = "LOADING"
28 | warnMode logMode = "WARN"
29 | debugMode logMode = "DEBUG"
30 | fatalMode logMode = "FATAL"
31 | )
32 |
33 | const (
34 | Reset = "\033[0m"
35 | Primary = "\033[33m" // Yellow (used for warnings)
36 | Secondary = "\033[36m" // Cyan (used for debug)
37 | Accent = "\033[37m" // White (used for loading)
38 | InfoColor = "\033[96m" // Bright Cyan (used for info)
39 | Success = "\033[32m" // Green (used for success)
40 | Warning = "\033[93m" // Bright Yellow (used for warnings)
41 | Error = "\033[31m" // Red (used for errors)
42 | FatalColor = "\033[35m" // Magenta (used for fatal)
43 | Dim = "\033[2m" // Dim text
44 | )
45 |
46 | type logger struct {
47 | Out io.Writer
48 | DebugMode bool
49 | mutex sync.Mutex
50 | }
51 |
52 | var Log *logger
53 |
54 | func init() {
55 | Log = NewLogger()
56 | }
57 |
58 | func NewLogger() *logger {
59 | return &logger{
60 | Out: os.Stdout,
61 | }
62 | }
63 |
64 | func (l *logger) prefix(mode logMode, color string) string {
65 | timeNow := fmt.Sprintf("%s%s%s", Accent, time.Now().Format(time.Kitchen), Reset)
66 | return fmt.Sprintf("[%s] %s%s%s ", timeNow, color, mode, Reset)
67 | }
68 |
69 | func (l *logger) log(mode logMode, color string, format string, args ...any) {
70 | l.mutex.Lock()
71 | defer l.mutex.Unlock()
72 | message := fmt.Sprintf(format, args...)
73 | _, err := l.Out.Write([]byte(l.prefix(mode, color) + Dim + message + Reset + "\n"))
74 | if err != nil {
75 | _, _ = fmt.Printf("Logging error: %v\n", err)
76 | }
77 | }
78 |
79 | func (l *logger) Info(format string, args ...any) {
80 | l.log(infoMode, InfoColor, format, args...)
81 | }
82 |
83 | func (l *logger) Success(format string, args ...any) {
84 | l.log(successMode, Success, format, args...)
85 | }
86 |
87 | func (l *logger) Error(format string, args ...any) {
88 | l.log(errorMode, Error, format, args...)
89 | }
90 |
91 | func (l *logger) Loading(format string, args ...any) {
92 | l.log(loadingMode, Accent, format, args...)
93 | }
94 |
95 | func (l *logger) Warn(format string, args ...any) {
96 | l.log(warnMode, Warning, format, args...)
97 | }
98 |
99 | func (l *logger) Debug(format string, args ...any) {
100 | if l.DebugMode {
101 | l.log(debugMode, Secondary, format, args...)
102 | }
103 | }
104 |
105 | func (l *logger) Fatal(format string, args ...any) {
106 | l.log(fatalMode, FatalColor, format, args...)
107 | os.Exit(1)
108 | }
109 |
--------------------------------------------------------------------------------
/views/pages/AuthRedirect_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.898
4 | package pages
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | import "morethancoder/t3-clone/views/components"
12 |
13 | func AuthRedirect(title string) templ.Component {
14 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
15 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
16 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
17 | return templ_7745c5c3_CtxErr
18 | }
19 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
20 | if !templ_7745c5c3_IsBuffer {
21 | defer func() {
22 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
23 | if templ_7745c5c3_Err == nil {
24 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
25 | }
26 | }()
27 | }
28 | ctx = templ.InitializeContext(ctx)
29 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
30 | if templ_7745c5c3_Var1 == nil {
31 | templ_7745c5c3_Var1 = templ.NopComponent
32 | }
33 | ctx = templ.ClearChildren(ctx)
34 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
")
35 | if templ_7745c5c3_Err != nil {
36 | return templ_7745c5c3_Err
37 | }
38 | templ_7745c5c3_Err = components.IconLoading("size-6").Render(ctx, templ_7745c5c3_Buffer)
39 | if templ_7745c5c3_Err != nil {
40 | return templ_7745c5c3_Err
41 | }
42 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
")
43 | if templ_7745c5c3_Err != nil {
44 | return templ_7745c5c3_Err
45 | }
46 | var templ_7745c5c3_Var2 string
47 | templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(title)
48 | if templ_7745c5c3_Err != nil {
49 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages/AuthRedirect.templ`, Line: 20, Col: 50}
50 | }
51 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
52 | if templ_7745c5c3_Err != nil {
53 | return templ_7745c5c3_Err
54 | }
55 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " Please wait a moment to be redirected
")
56 | if templ_7745c5c3_Err != nil {
57 | return templ_7745c5c3_Err
58 | }
59 | templ_7745c5c3_Err = components.BackgroundBlobs().Render(ctx, templ_7745c5c3_Buffer)
60 | if templ_7745c5c3_Err != nil {
61 | return templ_7745c5c3_Err
62 | }
63 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, " ")
64 | if templ_7745c5c3_Err != nil {
65 | return templ_7745c5c3_Err
66 | }
67 | return nil
68 | })
69 | }
70 |
71 | var _ = templruntime.GeneratedTemplate
72 |
--------------------------------------------------------------------------------
/views/components/ChatBubble.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | )
7 |
8 | templ Chat(bubbles []templ.Component) {
9 |
10 | for _, bubble := range bubbles {
11 | @bubble
12 | }
13 |
14 | }
15 |
16 | type ChatBubbleData struct {
17 | ID string
18 | Role string
19 | Type string
20 | Text string
21 | ImageUrl string
22 | Timestamp string
23 | Status string
24 | Model string
25 | Company string
26 | }
27 |
28 | templ ChatBubble(data ChatBubbleData) {
29 | if data.Role == "loading" {
30 |
31 |
32 | switch strings.ToLower(data.Company) {
33 | case "google":
34 | @LogoGemini("size-7 text-accent animate-spin glow")
35 | case "openai":
36 | @LogoGPT("size-7 text-accent animate-spin glow")
37 | case "anthropic":
38 | @LogoClaude("size-7 text-accent animate-spin glow")
39 | case "deepseek":
40 | @LogoDeepSeek("size-7 text-accent animate-spin glow")
41 | default:
42 | @LogoGPT("size-7 text-accent animate-spin glow")
43 | }
44 |
45 |
48 |
49 | Typing...
50 |
51 |
52 | } else if data.Role == "error" {
53 |
54 |
55 | switch strings.ToLower(data.Company) {
56 | case "google":
57 | @LogoGemini("size-7 text-accent animate-pulse glow")
58 | case "openai":
59 | @LogoGPT("size-7 text-accent animate-pulse glow")
60 | case "anthropic":
61 | @LogoClaude("size-7 text-accent animate-pulse glow")
62 | case "deepseek":
63 | @LogoDeepSeek("size-7 text-accent animate-pulse glow")
64 | default:
65 | @LogoGPT("size-7 text-accent animate-pulse glow")
66 | }
67 |
68 |
71 |
72 | { data.Text }
73 |
74 |
75 | } else if data.Role == "user" {
76 |
77 |
81 |
82 | { data.Text }
83 |
84 |
85 | } else {
86 |
87 |
88 | switch strings.ToLower(data.Company) {
89 | case "google":
90 | @LogoGemini("size-7 text-accent animate-pulse glow")
91 | case "openai":
92 | @LogoGPT("size-7 text-accent animate-pulse glow")
93 | case "anthropic":
94 | @LogoClaude("size-7 text-accent animate-pulse glow")
95 | case "deepseek":
96 | @LogoDeepSeek("size-7 text-accent animate-pulse glow")
97 | default:
98 | @LogoGPT("size-7 text-accent animate-pulse glow")
99 | }
100 |
101 |
105 |
109 | { data.Text }
110 |
111 |
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/views/components/Background_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.898
4 | package components
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | func BackgroundBlobs() templ.Component {
12 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
13 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
14 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
15 | return templ_7745c5c3_CtxErr
16 | }
17 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
18 | if !templ_7745c5c3_IsBuffer {
19 | defer func() {
20 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
21 | if templ_7745c5c3_Err == nil {
22 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
23 | }
24 | }()
25 | }
26 | ctx = templ.InitializeContext(ctx)
27 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
28 | if templ_7745c5c3_Var1 == nil {
29 | templ_7745c5c3_Var1 = templ.NopComponent
30 | }
31 | ctx = templ.ClearChildren(ctx)
32 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
33 | if templ_7745c5c3_Err != nil {
34 | return templ_7745c5c3_Err
35 | }
36 | return nil
37 | })
38 | }
39 |
40 | var _ = templruntime.GeneratedTemplate
41 |
--------------------------------------------------------------------------------
/handlers/auth.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "context"
5 | "morethancoder/t3-clone/db"
6 | "morethancoder/t3-clone/services"
7 | "morethancoder/t3-clone/utils"
8 | "morethancoder/t3-clone/views/layouts"
9 | "morethancoder/t3-clone/views/pages"
10 | "net/http"
11 | "os"
12 |
13 | datastar "github.com/starfederation/datastar/sdk/go"
14 | )
15 |
16 | func GETAuthRedirect() http.HandlerFunc {
17 | return func(w http.ResponseWriter, r *http.Request) {
18 | err := layouts.MainLayout(pages.AuthRedirect("Authenticating...")).Render(r.Context(), w)
19 | if err != nil {
20 | utils.Log.Error(err.Error())
21 | http.Error(w, "Something went wrong", http.StatusInternalServerError)
22 | return
23 | }
24 | }
25 | }
26 |
27 | func POSTAuthRedirect() http.HandlerFunc {
28 | return func(w http.ResponseWriter, r *http.Request) {
29 | _, err := r.Cookie("jwt")
30 | if err == nil {
31 | utils.Log.Info("Already authenticated")
32 | sse := datastar.NewSSE(w, r)
33 | sse.ExecuteScript("window.location.href = '/'")
34 | return
35 | }
36 | //get user session
37 | cookie, err := r.Cookie(KeySessionID)
38 | if err != nil {
39 | utils.Log.Error(err.Error())
40 | sse := datastar.NewSSE(w, r)
41 | sse.ExecuteScript("window.location.href = '/login'")
42 | return
43 | }
44 |
45 | //get search params
46 | state := r.URL.Query().Get("state")
47 | code := r.URL.Query().Get("code")
48 |
49 | session := services.Session{
50 | ID: cookie.Value,
51 | }
52 |
53 | ok := session.Load()
54 | if !ok {
55 | utils.Log.Error("Session not found")
56 | sse := datastar.NewSSE(w, r)
57 | sse.ExecuteScript("window.location.href = '/login'")
58 | return
59 | }
60 |
61 | //get auth providers
62 | authProviders, ok := session.Data[KeyOAuth2Providers].([]db.OAuth2Provider)
63 | if !ok {
64 | utils.Log.Error("Auth providers not found")
65 | sse := datastar.NewSSE(w, r)
66 | sse.ExecuteScript("window.location.href = '/login'")
67 | return
68 | }
69 |
70 | for _, provider := range authProviders {
71 | if provider.State == state {
72 | //authenticate
73 | utils.Log.Debug("Authenticating with %s code: %s", provider.Name, code)
74 |
75 | res, err := db.Db.AuthWithOAuth2(db.OAuth2AuthRequest{
76 | Provider: provider.Name,
77 | Code: code,
78 | CodeVerifier: provider.CodeVerifier,
79 | RedirectURL: os.Getenv("APP_URL") + "/auth-redirect",
80 | }, map[db.QueryParam]string{})
81 |
82 | if err != nil {
83 | utils.Log.Error("Failed to auth with %s: %s", provider.Name, err.Error())
84 | sse := datastar.NewSSE(w, r)
85 | sse.ExecuteScript("window.location.href = '/login'")
86 | return
87 | }
88 |
89 | // save jwt as cookie to auto auth
90 | jwtCookie := &http.Cookie{
91 | Name: "jwt",
92 | Value: res.Token,
93 | Path: "/",
94 | HttpOnly: true,
95 | SameSite: http.SameSiteStrictMode,
96 | }
97 |
98 | if os.Getenv("ENV") != "dev" {
99 | jwtCookie.Secure = true
100 | }
101 |
102 | modelrecords, err := db.Db.GetModelRecords(map[db.QueryParam]string{})
103 | if err != nil {
104 | utils.Log.Error(err.Error())
105 | http.Error(w, "Failed to get models", http.StatusInternalServerError)
106 | return
107 | }
108 |
109 | ctx := context.WithValue(r.Context(), "AuthRecord", res.Record)
110 | ctx = context.WithValue(ctx, "Models", db.GroupModelRecordsByCompany(modelrecords.Items))
111 | r = r.WithContext(ctx)
112 |
113 | http.SetCookie(w, jwtCookie)
114 |
115 | sse := datastar.NewSSE(w, r)
116 | //redirect to home
117 | err = sse.MergeFragmentTempl(pages.HomePage())
118 | if err != nil {
119 | utils.Log.Debug("Failed to merge fragment: %s", err.Error())
120 | sse.ExecuteScript("window.location.href = '/'")
121 | }
122 | return
123 |
124 | }
125 | }
126 |
127 | utils.Log.Error("State not found")
128 | sse := datastar.NewSSE(w, r)
129 | sse.ExecuteScript("window.location.href = '/login'")
130 | //maybe render an update in the frontend
131 | return
132 | }
133 | }
134 |
135 | func GETSignOut() http.HandlerFunc {
136 | return func(w http.ResponseWriter, r *http.Request) {
137 | http.SetCookie(w, &http.Cookie{
138 | Name: "jwt",
139 | Path: "/",
140 | MaxAge: -1,
141 | HttpOnly: true,
142 | SameSite: http.SameSiteStrictMode,
143 | })
144 | http.Redirect(w, r, "/login", http.StatusSeeOther)
145 | return
146 | }
147 | }
148 |
--------------------------------------------------------------------------------
/db/auth.go:
--------------------------------------------------------------------------------
1 | package db
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "errors"
7 | "fmt"
8 | "io"
9 | "net/http"
10 | "net/url"
11 | )
12 |
13 | type QueryParam string
14 |
15 | const (
16 | QueryParamExpand QueryParam = "expand"
17 | QueryParamFields QueryParam = "fields"
18 | )
19 |
20 | type AuthRecord struct {
21 | CollectionID string `json:"collectionId"`
22 | CollectionName string `json:"collectionName"`
23 | ID string `json:"id"`
24 | Email string `json:"email"`
25 | EmailVisibility bool `json:"emailVisibility"`
26 | Verified bool `json:"verified"`
27 | Name string `json:"name"`
28 | Avatar string `json:"avatar"`
29 | Created string `json:"created"`
30 | Updated string `json:"updated"`
31 | }
32 |
33 | type OAuth2AuthRequest struct {
34 | Provider string `json:"provider"`
35 | Code string `json:"code"`
36 | CodeVerifier string `json:"codeVerifier"`
37 | RedirectURL string `json:"redirectURL"`
38 | CreateData map[string]interface{} `json:"createData,omitempty"`
39 | }
40 |
41 | type OAuth2AuthResponse struct {
42 | Token string `json:"token"`
43 | Record AuthRecord `json:"record"`
44 | Meta struct {
45 | ID string `json:"id"`
46 | Name string `json:"name"`
47 | Username string `json:"username"`
48 | Email string `json:"email"`
49 | AvatarURL string `json:"avatarURL"`
50 | AccessToken string `json:"accessToken"`
51 | RefreshToken string `json:"refreshToken"`
52 | Expiry string `json:"expiry"`
53 | IsNew bool `json:"isNew"`
54 | RawUser map[string]interface{} `json:"rawUser"`
55 | } `json:"meta"`
56 | }
57 |
58 | func (db *db) AuthWithOAuth2(request OAuth2AuthRequest, queryParams map[QueryParam]string) (*OAuth2AuthResponse, error) {
59 | authEndpoint := "/api/collections/users/auth-with-oauth2"
60 |
61 | queryString := ""
62 | if len(queryParams) > 0 {
63 | query := url.Values{}
64 | for key, value := range queryParams {
65 | query.Add(string(key), value)
66 | }
67 | queryString = "?" + query.Encode()
68 | }
69 |
70 | requestBody, err := json.Marshal(request)
71 | if err != nil {
72 | return nil, err
73 | }
74 |
75 | req, err := http.NewRequest("POST", db.Url+authEndpoint+queryString, bytes.NewBuffer(requestBody))
76 | if err != nil {
77 | return nil, err
78 | }
79 |
80 | req.Header.Set("Content-Type", "application/json")
81 |
82 | resp, err := db.Client.Do(req)
83 | if err != nil {
84 | return nil, err
85 | }
86 | defer resp.Body.Close()
87 |
88 | respBody, err := io.ReadAll(resp.Body)
89 | if err != nil {
90 | return nil, err
91 | }
92 |
93 | if resp.StatusCode != http.StatusOK {
94 | return nil, errors.New(string(respBody))
95 | }
96 |
97 | var authResponse OAuth2AuthResponse
98 | if err := json.Unmarshal(respBody, &authResponse); err != nil {
99 | return nil, err
100 | }
101 |
102 | return &authResponse, nil
103 | }
104 |
105 |
106 | // AuthRefreshResponse represents the response structure for the auth-refresh endpoint
107 | type AuthRefreshResponse struct {
108 | Token string `json:"token"`
109 | Record AuthRecord `json:"record"`
110 | }
111 |
112 | func (d *db) AuthRefresh(token string) (*AuthRefreshResponse, error) {
113 | if token == "" {
114 | return nil, errors.New("[ERROR] Authorization token is required")
115 | }
116 |
117 | url := fmt.Sprintf("%s/api/collections/users/auth-refresh", d.Url)
118 |
119 | req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte{}))
120 | if err != nil {
121 | return nil, fmt.Errorf("[ERROR] failed to create request: %w", err)
122 | }
123 |
124 | req.Header.Set("Authorization", "Bearer "+token)
125 | req.Header.Set("Content-Type", "application/json")
126 |
127 | resp, err := d.Client.Do(req)
128 | if err != nil {
129 | return nil, fmt.Errorf("[ERROR] failed to make request: %w", err)
130 | }
131 | defer resp.Body.Close()
132 |
133 | if resp.StatusCode != http.StatusOK {
134 | body, _ := io.ReadAll(resp.Body)
135 | return nil, fmt.Errorf("[ERROR] request failed with status %d: %s", resp.StatusCode, string(body))
136 | }
137 |
138 | var authResponse AuthRefreshResponse
139 | if err := json.NewDecoder(resp.Body).Decode(&authResponse); err != nil {
140 | return nil, fmt.Errorf("[ERROR] failed to parse response: %w", err)
141 | }
142 |
143 | return &authResponse, nil
144 | }
145 |
--------------------------------------------------------------------------------
/services/openrouter.go:
--------------------------------------------------------------------------------
1 | package services
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "io"
8 | "morethancoder/t3-clone/utils"
9 | "net/http"
10 | "os"
11 | )
12 |
13 | type OpenRouterInstance struct {
14 | APIKey string
15 | Url string
16 | Client *http.Client
17 | }
18 |
19 | var OpenRouter = NewOpenRouter()
20 |
21 | func NewOpenRouter() *OpenRouterInstance {
22 | apiKey := os.Getenv("OPENROUTER_API_KEY")
23 | if apiKey == "" {
24 | utils.Log.Fatal("OPENROUTER_API_KEY is not set")
25 | }
26 | return &OpenRouterInstance{
27 | APIKey: apiKey,
28 | Url: "https://openrouter.ai/api/v1/chat/completions",
29 | Client: &http.Client{},
30 | }
31 | }
32 |
33 | type ImageURL struct {
34 | URL string `json:"url"`
35 | }
36 |
37 | type Content struct {
38 | Type string `json:"type"`
39 | Text string `json:"text,omitempty"`
40 | ImageURL *ImageURL `json:"image_url,omitempty"`
41 | }
42 |
43 | type Message struct {
44 | Role string `json:"role"`
45 | Content interface{} `json:"content"` // Can be string or []Content
46 | Refusal *string `json:"refusal"`
47 | Reasoning *string `json:"reasoning"`
48 | }
49 |
50 | type OpenRouterRequest struct {
51 | Model string `json:"model"`
52 | Messages []Message `json:"messages"`
53 | }
54 |
55 | type Choice struct {
56 | Logprobs interface{} `json:"logprobs"`
57 | FinishReason string `json:"finish_reason"`
58 | NativeFinishReason string `json:"native_finish_reason"`
59 | Index int `json:"index"`
60 | Message Message `json:"message"`
61 | }
62 |
63 | type UsageDetails struct {
64 | CachedTokens int `json:"cached_tokens"`
65 | }
66 |
67 | type CompletionTokensDetails struct {
68 | ReasoningTokens int `json:"reasoning_tokens"`
69 | }
70 |
71 | type Usage struct {
72 | PromptTokens int `json:"prompt_tokens"`
73 | CompletionTokens int `json:"completion_tokens"`
74 | TotalTokens int `json:"total_tokens"`
75 | PromptTokensDetails UsageDetails `json:"prompt_tokens_details"`
76 | CompletionTokensDetails CompletionTokensDetails `json:"completion_tokens_details"`
77 | }
78 |
79 | type OpenRouterResponse struct {
80 | ID string `json:"id"`
81 | Provider string `json:"provider"`
82 | Model string `json:"model"`
83 | Object string `json:"object"`
84 | Created int64 `json:"created"`
85 | Choices []Choice `json:"choices"`
86 | SystemFingerprint string `json:"system_fingerprint"`
87 | Usage Usage `json:"usage"`
88 | }
89 |
90 | func (o *OpenRouterInstance) Request(req OpenRouterRequest) (*OpenRouterResponse, error) {
91 | jsonData, err := json.Marshal(req)
92 | if err != nil {
93 | return nil, err
94 | }
95 |
96 | httpReq, err := http.NewRequest("POST", o.Url, bytes.NewBuffer(jsonData))
97 | if err != nil {
98 | return nil, err
99 | }
100 |
101 | httpReq.Header.Set("Content-Type", "application/json")
102 | httpReq.Header.Set("Authorization", "Bearer "+o.APIKey)
103 |
104 | httpRes, err := o.Client.Do(httpReq)
105 | if err != nil {
106 | return nil, err
107 | }
108 | defer httpRes.Body.Close()
109 |
110 | body, err := io.ReadAll(httpRes.Body)
111 | if err != nil {
112 | return nil, err
113 | }
114 |
115 | if httpRes.StatusCode != 200 {
116 | return nil, fmt.Errorf("non-200 status code: %d, body: %s", httpRes.StatusCode, body)
117 | }
118 |
119 | var response OpenRouterResponse
120 | if err := json.Unmarshal(body, &response); err != nil {
121 | return nil, fmt.Errorf("failed to unmarshal response: %w", err)
122 | }
123 |
124 | return &response, nil
125 | }
126 |
127 | func (o *OpenRouterInstance) RequestStream(req OpenRouterRequest, handler func(data []byte)) error {
128 | jsonData, err := json.Marshal(req)
129 | if err != nil {
130 | return err
131 | }
132 |
133 | httpReq, err := http.NewRequest("POST", o.Url, bytes.NewBuffer(jsonData))
134 | if err != nil {
135 | return err
136 | }
137 |
138 | httpReq.Header.Set("Content-Type", "application/json")
139 | httpReq.Header.Set("Authorization", "Bearer "+o.APIKey)
140 |
141 | httpRes, err := o.Client.Do(httpReq)
142 | if err != nil {
143 | return err
144 | }
145 | defer httpRes.Body.Close()
146 |
147 | if httpRes.StatusCode != 200 {
148 | body, _ := io.ReadAll(httpRes.Body)
149 | return fmt.Errorf("non-200 status code: %d, body: %s", httpRes.StatusCode, body)
150 | }
151 |
152 | decoder := json.NewDecoder(httpRes.Body)
153 | for {
154 | var chunk map[string]interface{}
155 | if err := decoder.Decode(&chunk); err == io.EOF {
156 | break
157 | } else if err != nil {
158 | return err
159 | }
160 |
161 | chunkData, err := json.Marshal(chunk)
162 | if err != nil {
163 | return err
164 | }
165 |
166 | handler(chunkData)
167 | }
168 |
169 | return nil
170 | }
171 |
--------------------------------------------------------------------------------
/views/components/Alert_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.898
4 | package components
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | type AlertData struct {
12 | Level string
13 | Message string
14 | Icon templ.Component
15 | }
16 |
17 | func Alert(data AlertData) templ.Component {
18 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
19 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
20 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
21 | return templ_7745c5c3_CtxErr
22 | }
23 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
24 | if !templ_7745c5c3_IsBuffer {
25 | defer func() {
26 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
27 | if templ_7745c5c3_Err == nil {
28 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
29 | }
30 | }()
31 | }
32 | ctx = templ.InitializeContext(ctx)
33 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
34 | if templ_7745c5c3_Var1 == nil {
35 | templ_7745c5c3_Var1 = templ.NopComponent
36 | }
37 | ctx = templ.ClearChildren(ctx)
38 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " $_showAlert=false, 5000)\" data-class=\"{ \n 'opacity-0 translate-y-[-100%]' : !$_showAlert,\n }\">")
94 | if templ_7745c5c3_Err != nil {
95 | return templ_7745c5c3_Err
96 | }
97 | if data.Icon != nil {
98 | templ_7745c5c3_Err = data.Icon.Render(ctx, templ_7745c5c3_Buffer)
99 | if templ_7745c5c3_Err != nil {
100 | return templ_7745c5c3_Err
101 | }
102 | }
103 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "")
104 | if templ_7745c5c3_Err != nil {
105 | return templ_7745c5c3_Err
106 | }
107 | var templ_7745c5c3_Var2 string
108 | templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(data.Message)
109 | if templ_7745c5c3_Err != nil {
110 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/Alert.templ`, Line: 51, Col: 22}
111 | }
112 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
113 | if templ_7745c5c3_Err != nil {
114 | return templ_7745c5c3_Err
115 | }
116 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
")
117 | if templ_7745c5c3_Err != nil {
118 | return templ_7745c5c3_Err
119 | }
120 | return nil
121 | })
122 | }
123 |
124 | var _ = templruntime.GeneratedTemplate
125 |
--------------------------------------------------------------------------------
/handlers/chat.go:
--------------------------------------------------------------------------------
1 | package handlers
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "morethancoder/t3-clone/db"
8 | "morethancoder/t3-clone/services"
9 | "morethancoder/t3-clone/utils"
10 | "morethancoder/t3-clone/views/components"
11 | "net/http"
12 | "strings"
13 | "time"
14 |
15 | "github.com/a-h/templ"
16 | "github.com/google/uuid"
17 | datastar "github.com/starfederation/datastar/sdk/go"
18 | "github.com/yuin/goldmark"
19 | )
20 |
21 | type Data struct {
22 | Chat []struct {
23 | Content []struct {
24 | Text string `json:"text"`
25 | Type string `json:"type"`
26 | } `json:"content"`
27 | Role string `json:"role"`
28 | } `json:"chat"`
29 | Model string `json:"model"`
30 | }
31 |
32 | func randomString() string {
33 | return uuid.NewString()
34 | }
35 |
36 | func GETNewChat(w http.ResponseWriter, r *http.Request) {
37 | jwtCookie, err := r.Cookie("jwt")
38 | if err != nil {
39 | utils.Log.Error("No jwt cookie found")
40 | http.Redirect(w, r, "/login", http.StatusSeeOther)
41 | return
42 | }
43 |
44 | authRefreshResponse, err := db.Db.AuthRefresh(jwtCookie.Value)
45 | if err != nil {
46 | utils.Log.Error(err.Error())
47 | http.SetCookie(w, &http.Cookie{
48 | Name: "jwt",
49 | MaxAge: -1,
50 | HttpOnly: true,
51 | })
52 | http.Error(w, "Failed to auth refresh", http.StatusUnauthorized)
53 | return
54 | }
55 | services.UserSSEHub.ExcuteScript(authRefreshResponse.Record.ID, "window.location.reload()")
56 | return
57 | }
58 |
59 | func POSTChat(w http.ResponseWriter, r *http.Request) {
60 | jwtCookie, err := r.Cookie("jwt")
61 | if err != nil {
62 | utils.Log.Error("No jwt cookie found")
63 | http.Redirect(w, r, "/login", http.StatusSeeOther)
64 | return
65 | }
66 |
67 | authRefreshResponse, err := db.Db.AuthRefresh(jwtCookie.Value)
68 | if err != nil {
69 | utils.Log.Error(err.Error())
70 | http.SetCookie(w, &http.Cookie{
71 | Name: "jwt",
72 | MaxAge: -1,
73 | HttpOnly: true,
74 | })
75 | http.Error(w, "Failed to auth refresh", http.StatusUnauthorized)
76 | return
77 | }
78 |
79 | userData := &Data{}
80 | err = datastar.ReadSignals(r, userData)
81 | if err != nil {
82 | utils.Log.Error(err.Error())
83 | return
84 | }
85 |
86 | var chatBubbles []templ.Component
87 | var msgs []services.Message
88 |
89 | for _, chat := range userData.Chat {
90 | chatBubbles = append(chatBubbles, components.ChatBubble(components.ChatBubbleData{
91 | ID: randomString(),
92 | Company: strings.Split(userData.Model, "/")[0],
93 | Role: chat.Role,
94 | Type: chat.Content[0].Type,
95 | Text: chat.Content[0].Text,
96 | Timestamp: time.Now().Format("3:04 PM"),
97 | Model: userData.Model,
98 | }))
99 | msgs = append(msgs, services.Message{
100 | Role: chat.Role,
101 | Content: []services.Content{services.Content{Type: chat.Content[0].Type, Text: chat.Content[0].Text}},
102 | })
103 | }
104 |
105 | chatBubbles = append(chatBubbles, components.ChatBubble(components.ChatBubbleData{
106 | ID: randomString(),
107 | Model: userData.Model,
108 | Role: "loading",
109 | Company: strings.Split(userData.Model, "/")[0],
110 | }))
111 |
112 | //rendering the prompt
113 | services.UserSSEHub.BroadcastFragments(authRefreshResponse.Record.ID,
114 | components.Chat(chatBubbles),
115 | )
116 |
117 | chatBubbles = chatBubbles[:len(chatBubbles)-1]
118 |
119 | req := services.OpenRouterRequest{
120 | Model: userData.Model,
121 | Messages: msgs,
122 | }
123 |
124 | res, err := services.OpenRouter.Request(req)
125 | if err != nil {
126 | utils.Log.Error(err.Error())
127 | chatBubbles = append(chatBubbles, components.ChatBubble(components.ChatBubbleData{
128 | ID: randomString(),
129 | Role: "assistant",
130 | Type: "text",
131 | Text: err.Error(),
132 | Company: strings.Split(userData.Model, "/")[0],
133 | Model: userData.Model,
134 | Timestamp: time.Now().Format("3:04 PM"),
135 | }))
136 |
137 | services.UserSSEHub.BroadcastFragments(authRefreshResponse.Record.ID,
138 | components.Chat(chatBubbles),
139 | )
140 |
141 | chatBubbles = chatBubbles[:len(chatBubbles)-1]
142 | return
143 | }
144 |
145 | for _, choice := range res.Choices {
146 | var buf bytes.Buffer
147 | err = goldmark.Convert([]byte(choice.Message.Content.(string)), &buf)
148 | if err != nil {
149 | utils.Log.Error(err.Error())
150 | return
151 | }
152 | chatBubbles = append(chatBubbles, components.ChatBubble(components.ChatBubbleData{
153 | ID: randomString(),
154 | Role: choice.Message.Role,
155 | Type: "text",
156 | Text: buf.String(),
157 | Company: res.Provider,
158 | Timestamp: time.Now().Format("3:04 PM"),
159 | Model: userData.Model,
160 | }))
161 | //update userdata chat
162 | userData.Chat = append(userData.Chat, struct {
163 | Content []struct {
164 | Text string "json:\"text\""
165 | Type string "json:\"type\""
166 | } "json:\"content\""
167 | Role string "json:\"role\""
168 | }{
169 | Role: "Assistance",
170 | Content: []struct {
171 | Text string "json:\"text\""
172 | Type string "json:\"type\""
173 | }{
174 | {
175 | Text: buf.String(),
176 | Type: choice.Message.Role,
177 | },
178 | },
179 | })
180 | }
181 |
182 | updatedChat, err := json.Marshal(userData.Chat)
183 | if err != nil {
184 | utils.Log.Error(err.Error())
185 | return
186 | }
187 | services.UserSSEHub.BroadcastSignals(authRefreshResponse.Record.ID, []byte(fmt.Sprintf(`{ chat : %s }`, string(updatedChat))))
188 |
189 | services.UserSSEHub.BroadcastFragments(authRefreshResponse.Record.ID,
190 | components.Chat(chatBubbles),
191 | )
192 | return
193 | }
194 |
--------------------------------------------------------------------------------
/views/pages/LoginPage_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.898
4 | package pages
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | import "morethancoder/t3-clone/views/components"
12 | import "context"
13 | import "morethancoder/t3-clone/db"
14 | import "os"
15 |
16 | func GetOAuth2Providers(ctx context.Context) []db.OAuth2Provider {
17 | data, ok := ctx.Value("OAuth2Providers").([]db.OAuth2Provider)
18 | if !ok {
19 | return []db.OAuth2Provider{}
20 | } else {
21 | return data
22 | }
23 | }
24 |
25 | func LoginPage() templ.Component {
26 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
27 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
28 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
29 | return templ_7745c5c3_CtxErr
30 | }
31 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
32 | if !templ_7745c5c3_IsBuffer {
33 | defer func() {
34 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
35 | if templ_7745c5c3_Err == nil {
36 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
37 | }
38 | }()
39 | }
40 | ctx = templ.InitializeContext(ctx)
41 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
42 | if templ_7745c5c3_Var1 == nil {
43 | templ_7745c5c3_Var1 = templ.NopComponent
44 | }
45 | ctx = templ.ClearChildren(ctx)
46 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "
")
47 | if templ_7745c5c3_Err != nil {
48 | return templ_7745c5c3_Err
49 | }
50 | templ_7745c5c3_Err = components.IconLockWithHole("size-5").Render(ctx, templ_7745c5c3_Buffer)
51 | if templ_7745c5c3_Err != nil {
52 | return templ_7745c5c3_Err
53 | }
54 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
Welcome Back Sign in to continue to your account
")
55 | if templ_7745c5c3_Err != nil {
56 | return templ_7745c5c3_Err
57 | }
58 | for _, provider := range GetOAuth2Providers(ctx) {
59 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "
")
73 | if templ_7745c5c3_Err != nil {
74 | return templ_7745c5c3_Err
75 | }
76 | switch provider.Name {
77 | case "google":
78 | templ_7745c5c3_Err = components.LogoGoogleColored("size-5").Render(ctx, templ_7745c5c3_Buffer)
79 | if templ_7745c5c3_Err != nil {
80 | return templ_7745c5c3_Err
81 | }
82 | case "github":
83 | templ_7745c5c3_Err = components.LogoGithub("size-5").Render(ctx, templ_7745c5c3_Buffer)
84 | if templ_7745c5c3_Err != nil {
85 | return templ_7745c5c3_Err
86 | }
87 | }
88 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Sign in with ")
89 | if templ_7745c5c3_Err != nil {
90 | return templ_7745c5c3_Err
91 | }
92 | var templ_7745c5c3_Var3 string
93 | templ_7745c5c3_Var3, templ_7745c5c3_Err = templ.JoinStringErrs(provider.DisplayName)
94 | if templ_7745c5c3_Err != nil {
95 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/pages/LoginPage.templ`, Line: 46, Col: 56}
96 | }
97 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var3))
98 | if templ_7745c5c3_Err != nil {
99 | return templ_7745c5c3_Err
100 | }
101 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, " ")
102 | if templ_7745c5c3_Err != nil {
103 | return templ_7745c5c3_Err
104 | }
105 | }
106 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
By continuing, you agree to our Terms of Service
")
107 | if templ_7745c5c3_Err != nil {
108 | return templ_7745c5c3_Err
109 | }
110 | templ_7745c5c3_Err = components.BackgroundBlobs().Render(ctx, templ_7745c5c3_Buffer)
111 | if templ_7745c5c3_Err != nil {
112 | return templ_7745c5c3_Err
113 | }
114 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "
")
115 | if templ_7745c5c3_Err != nil {
116 | return templ_7745c5c3_Err
117 | }
118 | return nil
119 | })
120 | }
121 |
122 | var _ = templruntime.GeneratedTemplate
123 |
--------------------------------------------------------------------------------
/views/components/Logos.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | templ LogoGithub(class string) {
4 |
6 |
8 |
9 |
10 | }
11 |
12 | templ LogoGemini(class string) {
13 |
14 |
16 |
17 |
18 | }
19 |
20 | templ LogoMeta(class string) {
21 |
22 |
23 |
25 |
26 |
27 | }
28 |
29 | templ LogoClaude(class string) {
30 |
31 |
32 |
34 |
35 |
36 | }
37 |
38 | templ LogoDeepSeek(class string) {
39 |
41 | DeepSeek
42 |
44 |
45 |
46 | }
47 |
48 | templ LogoGPT(class string) {
49 |
50 |
51 |
53 |
54 |
55 | }
56 |
57 | templ LogoGoogleColored(class string) {
58 |
60 |
62 |
63 |
65 |
66 |
68 |
69 |
71 |
72 |
74 |
75 |
76 | }
77 |
--------------------------------------------------------------------------------
/views/components/ChatBubble_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.898
4 | package components
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | import (
12 | "fmt"
13 | "strings"
14 | )
15 |
16 | func Chat(bubbles []templ.Component) templ.Component {
17 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
18 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
19 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
20 | return templ_7745c5c3_CtxErr
21 | }
22 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
23 | if !templ_7745c5c3_IsBuffer {
24 | defer func() {
25 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
26 | if templ_7745c5c3_Err == nil {
27 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
28 | }
29 | }()
30 | }
31 | ctx = templ.InitializeContext(ctx)
32 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
33 | if templ_7745c5c3_Var1 == nil {
34 | templ_7745c5c3_Var1 = templ.NopComponent
35 | }
36 | ctx = templ.ClearChildren(ctx)
37 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, "")
38 | if templ_7745c5c3_Err != nil {
39 | return templ_7745c5c3_Err
40 | }
41 | for _, bubble := range bubbles {
42 | templ_7745c5c3_Err = bubble.Render(ctx, templ_7745c5c3_Buffer)
43 | if templ_7745c5c3_Err != nil {
44 | return templ_7745c5c3_Err
45 | }
46 | }
47 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "
")
48 | if templ_7745c5c3_Err != nil {
49 | return templ_7745c5c3_Err
50 | }
51 | return nil
52 | })
53 | }
54 |
55 | type ChatBubbleData struct {
56 | ID string
57 | Role string
58 | Type string
59 | Text string
60 | ImageUrl string
61 | Timestamp string
62 | Status string
63 | Model string
64 | Company string
65 | }
66 |
67 | func ChatBubble(data ChatBubbleData) templ.Component {
68 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
69 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
70 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
71 | return templ_7745c5c3_CtxErr
72 | }
73 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
74 | if !templ_7745c5c3_IsBuffer {
75 | defer func() {
76 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
77 | if templ_7745c5c3_Err == nil {
78 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
79 | }
80 | }()
81 | }
82 | ctx = templ.InitializeContext(ctx)
83 | templ_7745c5c3_Var2 := templ.GetChildren(ctx)
84 | if templ_7745c5c3_Var2 == nil {
85 | templ_7745c5c3_Var2 = templ.NopComponent
86 | }
87 | ctx = templ.ClearChildren(ctx)
88 | if data.Role == "loading" {
89 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, "")
90 | if templ_7745c5c3_Err != nil {
91 | return templ_7745c5c3_Err
92 | }
93 | switch strings.ToLower(data.Company) {
94 | case "google":
95 | templ_7745c5c3_Err = LogoGemini("size-7 text-accent animate-spin glow").Render(ctx, templ_7745c5c3_Buffer)
96 | if templ_7745c5c3_Err != nil {
97 | return templ_7745c5c3_Err
98 | }
99 | case "openai":
100 | templ_7745c5c3_Err = LogoGPT("size-7 text-accent animate-spin glow").Render(ctx, templ_7745c5c3_Buffer)
101 | if templ_7745c5c3_Err != nil {
102 | return templ_7745c5c3_Err
103 | }
104 | case "anthropic":
105 | templ_7745c5c3_Err = LogoClaude("size-7 text-accent animate-spin glow").Render(ctx, templ_7745c5c3_Buffer)
106 | if templ_7745c5c3_Err != nil {
107 | return templ_7745c5c3_Err
108 | }
109 | case "deepseek":
110 | templ_7745c5c3_Err = LogoDeepSeek("size-7 text-accent animate-spin glow").Render(ctx, templ_7745c5c3_Buffer)
111 | if templ_7745c5c3_Err != nil {
112 | return templ_7745c5c3_Err
113 | }
114 | default:
115 | templ_7745c5c3_Err = LogoGPT("size-7 text-accent animate-spin glow").Render(ctx, templ_7745c5c3_Buffer)
116 | if templ_7745c5c3_Err != nil {
117 | return templ_7745c5c3_Err
118 | }
119 | }
120 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "
Typing...
")
134 | if templ_7745c5c3_Err != nil {
135 | return templ_7745c5c3_Err
136 | }
137 | } else if data.Role == "error" {
138 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "")
139 | if templ_7745c5c3_Err != nil {
140 | return templ_7745c5c3_Err
141 | }
142 | switch strings.ToLower(data.Company) {
143 | case "google":
144 | templ_7745c5c3_Err = LogoGemini("size-7 text-accent animate-pulse glow").Render(ctx, templ_7745c5c3_Buffer)
145 | if templ_7745c5c3_Err != nil {
146 | return templ_7745c5c3_Err
147 | }
148 | case "openai":
149 | templ_7745c5c3_Err = LogoGPT("size-7 text-accent animate-pulse glow").Render(ctx, templ_7745c5c3_Buffer)
150 | if templ_7745c5c3_Err != nil {
151 | return templ_7745c5c3_Err
152 | }
153 | case "anthropic":
154 | templ_7745c5c3_Err = LogoClaude("size-7 text-accent animate-pulse glow").Render(ctx, templ_7745c5c3_Buffer)
155 | if templ_7745c5c3_Err != nil {
156 | return templ_7745c5c3_Err
157 | }
158 | case "deepseek":
159 | templ_7745c5c3_Err = LogoDeepSeek("size-7 text-accent animate-pulse glow").Render(ctx, templ_7745c5c3_Buffer)
160 | if templ_7745c5c3_Err != nil {
161 | return templ_7745c5c3_Err
162 | }
163 | default:
164 | templ_7745c5c3_Err = LogoGPT("size-7 text-accent animate-pulse glow").Render(ctx, templ_7745c5c3_Buffer)
165 | if templ_7745c5c3_Err != nil {
166 | return templ_7745c5c3_Err
167 | }
168 | }
169 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, "
")
183 | if templ_7745c5c3_Err != nil {
184 | return templ_7745c5c3_Err
185 | }
186 | var templ_7745c5c3_Var5 string
187 | templ_7745c5c3_Var5, templ_7745c5c3_Err = templ.JoinStringErrs(data.Text)
188 | if templ_7745c5c3_Err != nil {
189 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/ChatBubble.templ`, Line: 72, Col: 15}
190 | }
191 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var5))
192 | if templ_7745c5c3_Err != nil {
193 | return templ_7745c5c3_Err
194 | }
195 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "
")
196 | if templ_7745c5c3_Err != nil {
197 | return templ_7745c5c3_Err
198 | }
199 | } else if data.Role == "user" {
200 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 10, "")
214 | if templ_7745c5c3_Err != nil {
215 | return templ_7745c5c3_Err
216 | }
217 | var templ_7745c5c3_Var7 string
218 | templ_7745c5c3_Var7, templ_7745c5c3_Err = templ.JoinStringErrs(data.Text)
219 | if templ_7745c5c3_Err != nil {
220 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/ChatBubble.templ`, Line: 82, Col: 15}
221 | }
222 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var7))
223 | if templ_7745c5c3_Err != nil {
224 | return templ_7745c5c3_Err
225 | }
226 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 12, "
")
227 | if templ_7745c5c3_Err != nil {
228 | return templ_7745c5c3_Err
229 | }
230 | } else {
231 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, "")
232 | if templ_7745c5c3_Err != nil {
233 | return templ_7745c5c3_Err
234 | }
235 | switch strings.ToLower(data.Company) {
236 | case "google":
237 | templ_7745c5c3_Err = LogoGemini("size-7 text-accent animate-pulse glow").Render(ctx, templ_7745c5c3_Buffer)
238 | if templ_7745c5c3_Err != nil {
239 | return templ_7745c5c3_Err
240 | }
241 | case "openai":
242 | templ_7745c5c3_Err = LogoGPT("size-7 text-accent animate-pulse glow").Render(ctx, templ_7745c5c3_Buffer)
243 | if templ_7745c5c3_Err != nil {
244 | return templ_7745c5c3_Err
245 | }
246 | case "anthropic":
247 | templ_7745c5c3_Err = LogoClaude("size-7 text-accent animate-pulse glow").Render(ctx, templ_7745c5c3_Buffer)
248 | if templ_7745c5c3_Err != nil {
249 | return templ_7745c5c3_Err
250 | }
251 | case "deepseek":
252 | templ_7745c5c3_Err = LogoDeepSeek("size-7 text-accent animate-pulse glow").Render(ctx, templ_7745c5c3_Buffer)
253 | if templ_7745c5c3_Err != nil {
254 | return templ_7745c5c3_Err
255 | }
256 | default:
257 | templ_7745c5c3_Err = LogoGPT("size-7 text-accent animate-pulse glow").Render(ctx, templ_7745c5c3_Buffer)
258 | if templ_7745c5c3_Err != nil {
259 | return templ_7745c5c3_Err
260 | }
261 | }
262 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 14, "
")
289 | if templ_7745c5c3_Err != nil {
290 | return templ_7745c5c3_Err
291 | }
292 | var templ_7745c5c3_Var10 = []any{"prose chat-bubble " + data.ID}
293 | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var10...)
294 | if templ_7745c5c3_Err != nil {
295 | return templ_7745c5c3_Err
296 | }
297 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 17, "
")
324 | if templ_7745c5c3_Err != nil {
325 | return templ_7745c5c3_Err
326 | }
327 | var templ_7745c5c3_Var13 string
328 | templ_7745c5c3_Var13, templ_7745c5c3_Err = templ.JoinStringErrs(data.Text)
329 | if templ_7745c5c3_Err != nil {
330 | return templ.Error{Err: templ_7745c5c3_Err, FileName: `views/components/ChatBubble.templ`, Line: 109, Col: 15}
331 | }
332 | _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var13))
333 | if templ_7745c5c3_Err != nil {
334 | return templ_7745c5c3_Err
335 | }
336 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 20, "
")
337 | if templ_7745c5c3_Err != nil {
338 | return templ_7745c5c3_Err
339 | }
340 | }
341 | return nil
342 | })
343 | }
344 |
345 | var _ = templruntime.GeneratedTemplate
346 |
--------------------------------------------------------------------------------
/views/components/Icons.templ:
--------------------------------------------------------------------------------
1 | package components
2 |
3 | templ IconArrowUp(class string) {
4 |
5 |
6 |
14 |
15 | }
16 |
17 | templ IconSpeaker(class string) {
18 |
19 |
20 |
28 |
29 | }
30 |
31 | templ IconPlus(class string) {
32 |
33 |
34 |
42 |
43 | }
44 |
45 | templ IconWeb(class string) {
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 | }
54 |
55 | templ IconBrain(class string) {
56 |
57 |
58 |
59 |
60 |
61 |
64 |
65 |
66 | }
67 |
68 | templ IconPaperclip(class string) {
69 |
70 |
71 |
79 |
80 | }
81 |
82 |
83 |
84 | templ IconSearch(class string) {
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 | }
93 |
94 | templ IconGrad(class string) {
95 |
96 |
97 |
98 |
101 |
102 |
103 |
104 | }
105 |
106 | templ IconCode(class string) {
107 |
108 |
109 |
117 |
118 | }
119 |
120 | templ IconPlane(class string) {
121 |
122 |
123 |
131 |
132 | }
133 |
134 | templ IconChange(class string) {
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 | }
144 |
145 | templ IconLaugh(class string) {
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 | }
154 |
155 | templ IconList(class string) {
156 |
157 |
158 |
166 |
167 | }
168 |
169 | templ IconSquarePen(class string) {
170 |
171 |
172 |
173 |
174 |
177 |
178 |
179 | }
180 |
181 | templ IconPenTool(class string) {
182 |
183 |
184 |
185 |
188 |
191 |
192 |
193 |
194 | }
195 |
196 | templ IconNewspaper(class string) {
197 |
198 |
199 |
200 |
203 |
204 |
205 |
206 | }
207 |
208 | templ IconChevronDown(class string) {
209 |
210 |
211 |
219 |
220 | }
221 |
222 | templ IconHistory(class string) {
223 |
224 | }
225 |
226 | templ IconBookmark(class string) {
227 |
228 | }
229 |
230 | templ IconSettings(class string) {
231 |
232 | }
233 |
234 | templ IconPaintRoller(class string) {
235 |
236 | }
237 |
238 | templ IconKeyboard(class string) {
239 |
240 | }
241 |
242 | templ IconCPU(class string) {
243 |
244 | }
245 |
246 | templ IconX(class string) {
247 |
248 | }
249 |
250 | templ IconPin(class string) {
251 |
252 | }
253 |
254 | templ IconUnPin(class string) {
255 |
256 | }
257 |
258 | templ IconEye(class string) {
259 |
260 | }
261 |
262 | templ IconFileInput(class string) {
263 |
264 | }
265 |
266 | templ IconImagePlus(class string) {
267 |
268 | }
269 |
270 |
271 |
272 | templ IconLockWithHole(class string) {
273 |
274 | }
275 |
276 | templ IconLoading(class string) {
277 |
278 | }
279 |
280 | templ IconCircleAlert(class string) {
281 |
282 | }
283 |
284 | templ IconCircleCheck(class string) {
285 |
286 | }
287 |
288 | templ IconTriangleAlert(class string) {
289 |
290 | }
291 |
292 | templ IconCircleX(class string) {
293 |
294 | }
295 |
--------------------------------------------------------------------------------
/views/components/Logos_templ.go:
--------------------------------------------------------------------------------
1 | // Code generated by templ - DO NOT EDIT.
2 |
3 | // templ: version: v0.3.898
4 | package components
5 |
6 | //lint:file-ignore SA4006 This context is only used if a nested component is present.
7 |
8 | import "github.com/a-h/templ"
9 | import templruntime "github.com/a-h/templ/runtime"
10 |
11 | func LogoGithub(class string) templ.Component {
12 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
13 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
14 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
15 | return templ_7745c5c3_CtxErr
16 | }
17 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
18 | if !templ_7745c5c3_IsBuffer {
19 | defer func() {
20 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
21 | if templ_7745c5c3_Err == nil {
22 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
23 | }
24 | }()
25 | }
26 | ctx = templ.InitializeContext(ctx)
27 | templ_7745c5c3_Var1 := templ.GetChildren(ctx)
28 | if templ_7745c5c3_Var1 == nil {
29 | templ_7745c5c3_Var1 = templ.NopComponent
30 | }
31 | ctx = templ.ClearChildren(ctx)
32 | var templ_7745c5c3_Var2 = []any{class}
33 | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var2...)
34 | if templ_7745c5c3_Err != nil {
35 | return templ_7745c5c3_Err
36 | }
37 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 1, " ")
51 | if templ_7745c5c3_Err != nil {
52 | return templ_7745c5c3_Err
53 | }
54 | return nil
55 | })
56 | }
57 |
58 | func LogoGemini(class string) templ.Component {
59 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
60 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
61 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
62 | return templ_7745c5c3_CtxErr
63 | }
64 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
65 | if !templ_7745c5c3_IsBuffer {
66 | defer func() {
67 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
68 | if templ_7745c5c3_Err == nil {
69 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
70 | }
71 | }()
72 | }
73 | ctx = templ.InitializeContext(ctx)
74 | templ_7745c5c3_Var4 := templ.GetChildren(ctx)
75 | if templ_7745c5c3_Var4 == nil {
76 | templ_7745c5c3_Var4 = templ.NopComponent
77 | }
78 | ctx = templ.ClearChildren(ctx)
79 | var templ_7745c5c3_Var5 = []any{class}
80 | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var5...)
81 | if templ_7745c5c3_Err != nil {
82 | return templ_7745c5c3_Err
83 | }
84 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 3, " ")
98 | if templ_7745c5c3_Err != nil {
99 | return templ_7745c5c3_Err
100 | }
101 | return nil
102 | })
103 | }
104 |
105 | func LogoMeta(class string) templ.Component {
106 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
107 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
108 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
109 | return templ_7745c5c3_CtxErr
110 | }
111 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
112 | if !templ_7745c5c3_IsBuffer {
113 | defer func() {
114 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
115 | if templ_7745c5c3_Err == nil {
116 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
117 | }
118 | }()
119 | }
120 | ctx = templ.InitializeContext(ctx)
121 | templ_7745c5c3_Var7 := templ.GetChildren(ctx)
122 | if templ_7745c5c3_Var7 == nil {
123 | templ_7745c5c3_Var7 = templ.NopComponent
124 | }
125 | ctx = templ.ClearChildren(ctx)
126 | var templ_7745c5c3_Var8 = []any{class}
127 | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var8...)
128 | if templ_7745c5c3_Err != nil {
129 | return templ_7745c5c3_Err
130 | }
131 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, " ")
145 | if templ_7745c5c3_Err != nil {
146 | return templ_7745c5c3_Err
147 | }
148 | return nil
149 | })
150 | }
151 |
152 | func LogoClaude(class string) templ.Component {
153 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
154 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
155 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
156 | return templ_7745c5c3_CtxErr
157 | }
158 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
159 | if !templ_7745c5c3_IsBuffer {
160 | defer func() {
161 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
162 | if templ_7745c5c3_Err == nil {
163 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
164 | }
165 | }()
166 | }
167 | ctx = templ.InitializeContext(ctx)
168 | templ_7745c5c3_Var10 := templ.GetChildren(ctx)
169 | if templ_7745c5c3_Var10 == nil {
170 | templ_7745c5c3_Var10 = templ.NopComponent
171 | }
172 | ctx = templ.ClearChildren(ctx)
173 | var templ_7745c5c3_Var11 = []any{class}
174 | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var11...)
175 | if templ_7745c5c3_Err != nil {
176 | return templ_7745c5c3_Err
177 | }
178 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 7, " ")
192 | if templ_7745c5c3_Err != nil {
193 | return templ_7745c5c3_Err
194 | }
195 | return nil
196 | })
197 | }
198 |
199 | func LogoDeepSeek(class string) templ.Component {
200 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
201 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
202 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
203 | return templ_7745c5c3_CtxErr
204 | }
205 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
206 | if !templ_7745c5c3_IsBuffer {
207 | defer func() {
208 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
209 | if templ_7745c5c3_Err == nil {
210 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
211 | }
212 | }()
213 | }
214 | ctx = templ.InitializeContext(ctx)
215 | templ_7745c5c3_Var13 := templ.GetChildren(ctx)
216 | if templ_7745c5c3_Var13 == nil {
217 | templ_7745c5c3_Var13 = templ.NopComponent
218 | }
219 | ctx = templ.ClearChildren(ctx)
220 | var templ_7745c5c3_Var14 = []any{class}
221 | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var14...)
222 | if templ_7745c5c3_Err != nil {
223 | return templ_7745c5c3_Err
224 | }
225 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 9, "DeepSeek ")
239 | if templ_7745c5c3_Err != nil {
240 | return templ_7745c5c3_Err
241 | }
242 | return nil
243 | })
244 | }
245 |
246 | func LogoGPT(class string) templ.Component {
247 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
248 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
249 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
250 | return templ_7745c5c3_CtxErr
251 | }
252 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
253 | if !templ_7745c5c3_IsBuffer {
254 | defer func() {
255 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
256 | if templ_7745c5c3_Err == nil {
257 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
258 | }
259 | }()
260 | }
261 | ctx = templ.InitializeContext(ctx)
262 | templ_7745c5c3_Var16 := templ.GetChildren(ctx)
263 | if templ_7745c5c3_Var16 == nil {
264 | templ_7745c5c3_Var16 = templ.NopComponent
265 | }
266 | ctx = templ.ClearChildren(ctx)
267 | var templ_7745c5c3_Var17 = []any{class}
268 | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var17...)
269 | if templ_7745c5c3_Err != nil {
270 | return templ_7745c5c3_Err
271 | }
272 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 11, " ")
286 | if templ_7745c5c3_Err != nil {
287 | return templ_7745c5c3_Err
288 | }
289 | return nil
290 | })
291 | }
292 |
293 | func LogoGoogleColored(class string) templ.Component {
294 | return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
295 | templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
296 | if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
297 | return templ_7745c5c3_CtxErr
298 | }
299 | templ_7745c5c3_Buffer, templ_7745c5c3_IsBuffer := templruntime.GetBuffer(templ_7745c5c3_W)
300 | if !templ_7745c5c3_IsBuffer {
301 | defer func() {
302 | templ_7745c5c3_BufErr := templruntime.ReleaseBuffer(templ_7745c5c3_Buffer)
303 | if templ_7745c5c3_Err == nil {
304 | templ_7745c5c3_Err = templ_7745c5c3_BufErr
305 | }
306 | }()
307 | }
308 | ctx = templ.InitializeContext(ctx)
309 | templ_7745c5c3_Var19 := templ.GetChildren(ctx)
310 | if templ_7745c5c3_Var19 == nil {
311 | templ_7745c5c3_Var19 = templ.NopComponent
312 | }
313 | ctx = templ.ClearChildren(ctx)
314 | var templ_7745c5c3_Var20 = []any{class}
315 | templ_7745c5c3_Err = templ.RenderCSSItems(ctx, templ_7745c5c3_Buffer, templ_7745c5c3_Var20...)
316 | if templ_7745c5c3_Err != nil {
317 | return templ_7745c5c3_Err
318 | }
319 | templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 13, " ")
333 | if templ_7745c5c3_Err != nil {
334 | return templ_7745c5c3_Err
335 | }
336 | return nil
337 | })
338 | }
339 |
340 | var _ = templruntime.GeneratedTemplate
341 |
--------------------------------------------------------------------------------
/views/pages/HomePage.templ:
--------------------------------------------------------------------------------
1 | package pages
2 |
3 | import (
4 | "fmt"
5 | "morethancoder/t3-clone/views/components"
6 | "context"
7 | "morethancoder/t3-clone/db"
8 | "strings"
9 | )
10 |
11 | func GetAuthRecord(ctx context.Context) db.AuthRecord {
12 | data,ok := ctx.Value("AuthRecord").(db.AuthRecord)
13 | if !ok {
14 | return db.AuthRecord{}
15 | }
16 | return data
17 | }
18 |
19 | func GetModelsMap(ctx context.Context) map[string][]db.ModelRecord {
20 | data, ok := ctx.Value("Models").(map[string][]db.ModelRecord)
21 | if !ok {
22 | return map[string][]db.ModelRecord{
23 | "google": []db.ModelRecord{
24 | {ID: "gpt-3.5-turbo", Name: "GPT-3.5-turbo"},
25 | },
26 | }
27 | }
28 | return data
29 | }
30 |
31 |
32 | templ HomePage() {
33 |
39 |
40 |
41 | @HistoryBar()
42 | @ActionBar()
43 | @PromptBar()
44 | @MainSection(Starter())
45 |
46 |
47 | }
48 |
49 | templ MainSection(content templ.Component) {
50 |
52 |
53 | @content
54 |
55 |
56 | }
57 |
58 | templ Starter() {
59 |
62 |
63 |
64 | @components.LogoGemini("size-8 text-accent animate-pulse glow")
65 |
66 |
67 |
107 |
108 | Good Morning!
109 | How can I help you!
110 |
111 |
112 | @StarterCard(components.IconSearch("size-5"),
113 | "What are the latest trends in AI?")
114 | @StarterCard(components.IconGrad("size-5"),
115 | "Tell me the most important 20% of crypto so that I understand 80% of it.")
116 | @StarterCard(components.IconCode("size-5"),
117 | "Write a Python script to convert a CSV file to a JSON file.")
118 | @StarterCard(components.IconPlane("size-5"),
119 | "What are the must-see attractions in Japan?")
120 |
121 |
122 |
123 |
124 | @components.IconChange("size-5")
125 |
126 |
127 |
128 |
129 | @components.IconLaugh("size-5")
130 |
131 |
132 |
133 |
134 | @components.IconPenTool("size-5")
135 |
136 |
137 |
138 |
139 | @components.IconNewspaper("size-5")
140 |
141 |
142 |
143 |
144 | @components.IconList("size-5")
145 |
146 |
147 |
148 |
149 | }
150 |
151 | templ StarterCard(icon templ.Component, text string) {
152 |
156 |
157 | @icon
158 |
{ text }
159 |
160 |
161 | @components.IconArrowUp("size-5")
162 |
163 |
164 |
165 |
166 | }
167 |
168 |
169 |
170 |
171 |
172 | templ HistoryBar() {
173 |
177 |
178 |
179 | @components.IconSquarePen("size-5")
180 |
181 |
182 | @SlideCard(SlideCardData{
183 | dir: "left",
184 | id: "history",
185 | title: "History",
186 | icon: components.IconHistory("size-5"),
187 | content: History(),
188 | })
189 | @SlideCard(SlideCardData{
190 | dir: "left",
191 | id: "bookmarks",
192 | title: "Bookmarks",
193 | icon: components.IconBookmark("size-5"),
194 | content: Bookmarks(),
195 | })
196 |
197 | }
198 |
199 | templ Bookmarks() {
200 | Bookmarks
201 |
202 | @components.IconSearch("size-4")
203 |
204 |
205 |
210 |
211 | }
212 |
213 | templ History() {
214 | History
215 |
216 | @components.IconSearch("size-4")
217 |
218 |
219 |
243 | }
244 |
245 | templ HistoryItem(text string) {
246 |
249 |
250 | { text }
251 |
252 |
257 |
258 | @components.IconPin("size-4")
259 |
260 |
261 | @components.IconX("size-4")
262 |
263 |
264 |
265 |
266 | }
267 |
268 | type SlideCardData struct {
269 | id, title, dir string
270 | icon templ.Component
271 | content templ.Component
272 | }
273 |
274 | templ SlideCard(data SlideCardData) {
275 |
280 |
291 |
299 | @data.icon
300 |
301 |
302 |
{$_%sHover=false}, 300)",data.id) }
305 |
306 |
307 |
308 | if data.dir == "left" {
309 |
310 | data-class={ fmt.Sprintf(` {
311 | 'opacity-100 translate-x-0' : ($_%[1]sClick||$_%[1]sHover),
312 | 'opacity-0 translate-x-[-120%%]' : (!$_%[1]sClick && !$_%[1]sHover)
313 | }`, data.id) }
314 |
315 | class="card card-md bg-base-100/50 backdrop-blur
316 | border-1 border-base-content/10
317 | shadow-sm shadow-base-content/20
318 | rounded-2xl z-10
319 | absolute top-0 left-0
320 | transition-all duration-300 translate-x-[-120%]
321 | opacity-0
322 | transform"
323 | } else {
324 | data-class={ fmt.Sprintf(` {
325 | 'opacity-100 translate-x-0' : ($_%[1]sClick||$_%[1]sHover),
326 | 'opacity-0 translate-x-[120%%]' : (!$_%[1]sClick && !$_%[1]sHover)
327 | }`, data.id) }
328 |
329 | class="card card-md bg-base-100/50 backdrop-blur
330 | border-1 border-base-content/10
331 | shadow-sm shadow-base-content/20
332 | rounded-2xl z-10
333 | absolute top-0 right-0
334 | transition-all duration-300 translate-x-[120%]
335 | opacity-0
336 | transform"
337 | }
338 |
339 | >
340 |
341 | @data.content
342 |
343 |
344 |
345 | }
346 |
347 | templ Settings() {
348 | Settings
349 |
411 | }
412 |
413 | templ Theme() {
414 | Theme
415 | }
416 |
417 | templ Keybinds() {
418 | Keybinds
419 | }
420 |
421 |
422 | type ModelCardData struct {
423 | id string
424 | name string
425 | company string
426 | badges []templ.Component
427 | color Color
428 | }
429 | templ ModelCard(data ModelCardData) {
430 |
483 |
484 |
485 |
{ strings.ReplaceAll(strings.Split(data.name, "/")[1], "-", " ") }
486 |
487 |
488 | switch data.company {
489 | case "openai":
490 | @components.LogoGPT("size-7")
491 | case "anthropic":
492 | @components.LogoClaude("size-7")
493 | case "google":
494 | @components.LogoGemini("size-7")
495 | case "deepseek":
496 | @components.LogoDeepSeek("size-7")
497 | default:
498 | @components.LogoGPT("size-7")
499 | }
500 |
501 |
502 |
503 |
504 | for _,badge := range data.badges {
505 |
506 | @badge
507 |
508 | }
509 |
510 |
511 |
512 |
513 | }
514 |
515 | //--Badge--
516 | //TODO: Move to components
517 | type Color string
518 |
519 | const (
520 | ColorPrimary Color = "primary"
521 | ColorSecondary Color = "secondary"
522 | ColorAccent Color = "accent"
523 | ColorNeutral Color = "neutral"
524 | ColorInfo Color = "info"
525 | ColorSuccess Color = "success"
526 | ColorWarning Color = "warning"
527 | ColorError Color = "error"
528 | )
529 |
530 | type BadgeData struct {
531 | text string
532 | icon templ.Component
533 | color Color
534 | }
535 |
536 | templ Badge(data BadgeData) {
537 |
563 |
564 | }
565 | //End --Badge--
566 |
567 | templ Models() {
568 | Models
569 |
570 | @components.IconSearch("size-4")
571 |
572 |
573 |
590 |
591 | }
592 |
593 |
594 | templ ActionBar() {
595 |
598 |
599 |
600 | @components.IconSpeaker("size-5")
601 |
602 |
603 | @SlideCard(SlideCardData{
604 | dir: "right",
605 | id: "settings",
606 | title: "Settings",
607 | icon: components.IconSettings("size-5"),
608 | content: Settings(),
609 | })
610 |
611 | @SlideCard(SlideCardData{
612 | dir: "right",
613 | id: "theme",
614 | title: "Theme",
615 | icon: components.IconPaintRoller("size-5"),
616 | content: Theme(),
617 | })
618 |
619 | @SlideCard(SlideCardData{
620 | dir: "right",
621 | id: "keybinds",
622 | title: "Keybinds",
623 | icon: components.IconKeyboard("size-5"),
624 | content: Keybinds(),
625 | })
626 |
627 | @SlideCard(SlideCardData{
628 | dir: "right",
629 | id: "models",
630 | title: "Models",
631 | icon: components.IconCPU("size-5"),
632 | content: Models(),
633 | })
634 |
635 |
636 | }
637 |
638 | templ PromptBar() {
639 |
645 |
646 |
647 |
648 |
649 | @components.IconPaperclip("size-5")
650 |
651 |
652 |
653 |
654 |
655 |
656 | @components.IconBrain("size-5")
657 |
658 |
659 |
660 |
661 |
662 |
663 | @components.IconWeb("size-5")
664 |
665 |
666 |
667 |
684 |
685 |
686 |
693 | @components.IconArrowUp("size-5")
694 |
695 |
696 |
697 | }
698 |
--------------------------------------------------------------------------------
/pb_schema.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "id": "pbc_3142635823",
4 | "listRule": null,
5 | "viewRule": null,
6 | "createRule": null,
7 | "updateRule": null,
8 | "deleteRule": null,
9 | "name": "_superusers",
10 | "type": "auth",
11 | "fields": [
12 | {
13 | "autogeneratePattern": "[a-z0-9]{15}",
14 | "hidden": false,
15 | "id": "text3208210256",
16 | "max": 15,
17 | "min": 15,
18 | "name": "id",
19 | "pattern": "^[a-z0-9]+$",
20 | "presentable": false,
21 | "primaryKey": true,
22 | "required": true,
23 | "system": true,
24 | "type": "text"
25 | },
26 | {
27 | "cost": 0,
28 | "hidden": true,
29 | "id": "password901924565",
30 | "max": 0,
31 | "min": 8,
32 | "name": "password",
33 | "pattern": "",
34 | "presentable": false,
35 | "required": true,
36 | "system": true,
37 | "type": "password"
38 | },
39 | {
40 | "autogeneratePattern": "[a-zA-Z0-9]{50}",
41 | "hidden": true,
42 | "id": "text2504183744",
43 | "max": 60,
44 | "min": 30,
45 | "name": "tokenKey",
46 | "pattern": "",
47 | "presentable": false,
48 | "primaryKey": false,
49 | "required": true,
50 | "system": true,
51 | "type": "text"
52 | },
53 | {
54 | "exceptDomains": null,
55 | "hidden": false,
56 | "id": "email3885137012",
57 | "name": "email",
58 | "onlyDomains": null,
59 | "presentable": false,
60 | "required": true,
61 | "system": true,
62 | "type": "email"
63 | },
64 | {
65 | "hidden": false,
66 | "id": "bool1547992806",
67 | "name": "emailVisibility",
68 | "presentable": false,
69 | "required": false,
70 | "system": true,
71 | "type": "bool"
72 | },
73 | {
74 | "hidden": false,
75 | "id": "bool256245529",
76 | "name": "verified",
77 | "presentable": false,
78 | "required": false,
79 | "system": true,
80 | "type": "bool"
81 | },
82 | {
83 | "hidden": false,
84 | "id": "autodate2990389176",
85 | "name": "created",
86 | "onCreate": true,
87 | "onUpdate": false,
88 | "presentable": false,
89 | "system": true,
90 | "type": "autodate"
91 | },
92 | {
93 | "hidden": false,
94 | "id": "autodate3332085495",
95 | "name": "updated",
96 | "onCreate": true,
97 | "onUpdate": true,
98 | "presentable": false,
99 | "system": true,
100 | "type": "autodate"
101 | }
102 | ],
103 | "indexes": [
104 | "CREATE UNIQUE INDEX `idx_tokenKey_pbc_3142635823` ON `_superusers` (`tokenKey`)",
105 | "CREATE UNIQUE INDEX `idx_email_pbc_3142635823` ON `_superusers` (`email`) WHERE `email` != ''"
106 | ],
107 | "system": true,
108 | "authRule": "",
109 | "manageRule": null,
110 | "authAlert": {
111 | "enabled": true,
112 | "emailTemplate": {
113 | "subject": "Login from a new location",
114 | "body": "Hello,
\nWe noticed a login to your {APP_NAME} account from a new location.
\nIf this was you, you may disregard this email.
\nIf this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.
\n\n Thanks, \n {APP_NAME} team\n
"
115 | }
116 | },
117 | "oauth2": {
118 | "mappedFields": {
119 | "id": "",
120 | "name": "",
121 | "username": "",
122 | "avatarURL": ""
123 | },
124 | "enabled": false
125 | },
126 | "passwordAuth": {
127 | "enabled": true,
128 | "identityFields": [
129 | "email"
130 | ]
131 | },
132 | "mfa": {
133 | "enabled": false,
134 | "duration": 1800,
135 | "rule": ""
136 | },
137 | "otp": {
138 | "enabled": false,
139 | "duration": 180,
140 | "length": 8,
141 | "emailTemplate": {
142 | "subject": "OTP for {APP_NAME}",
143 | "body": "Hello,
\nYour one-time password is: {OTP}
\nIf you didn't ask for the one-time password, you can ignore this email.
\n\n Thanks, \n {APP_NAME} team\n
"
144 | }
145 | },
146 | "authToken": {
147 | "duration": 86400
148 | },
149 | "passwordResetToken": {
150 | "duration": 1800
151 | },
152 | "emailChangeToken": {
153 | "duration": 1800
154 | },
155 | "verificationToken": {
156 | "duration": 259200
157 | },
158 | "fileToken": {
159 | "duration": 180
160 | },
161 | "verificationTemplate": {
162 | "subject": "Verify your {APP_NAME} email",
163 | "body": "Hello,
\nThank you for joining us at {APP_NAME}.
\nClick on the button below to verify your email address.
\n\n Verify \n
\n\n Thanks, \n {APP_NAME} team\n
"
164 | },
165 | "resetPasswordTemplate": {
166 | "subject": "Reset your {APP_NAME} password",
167 | "body": "Hello,
\nClick on the button below to reset your password.
\n\n Reset password \n
\nIf you didn't ask to reset your password, you can ignore this email.
\n\n Thanks, \n {APP_NAME} team\n
"
168 | },
169 | "confirmEmailChangeTemplate": {
170 | "subject": "Confirm your {APP_NAME} new email address",
171 | "body": "Hello,
\nClick on the button below to confirm your new email address.
\n\n Confirm new email \n
\nIf you didn't ask to change your email address, you can ignore this email.
\n\n Thanks, \n {APP_NAME} team\n
"
172 | }
173 | },
174 | {
175 | "id": "_pb_users_auth_",
176 | "listRule": "id = @request.auth.id",
177 | "viewRule": "id = @request.auth.id",
178 | "createRule": "",
179 | "updateRule": "id = @request.auth.id",
180 | "deleteRule": "id = @request.auth.id",
181 | "name": "users",
182 | "type": "auth",
183 | "fields": [
184 | {
185 | "autogeneratePattern": "[a-z0-9]{15}",
186 | "hidden": false,
187 | "id": "text3208210256",
188 | "max": 15,
189 | "min": 15,
190 | "name": "id",
191 | "pattern": "^[a-z0-9]+$",
192 | "presentable": false,
193 | "primaryKey": true,
194 | "required": true,
195 | "system": true,
196 | "type": "text"
197 | },
198 | {
199 | "cost": 0,
200 | "hidden": true,
201 | "id": "password901924565",
202 | "max": 0,
203 | "min": 8,
204 | "name": "password",
205 | "pattern": "",
206 | "presentable": false,
207 | "required": true,
208 | "system": true,
209 | "type": "password"
210 | },
211 | {
212 | "autogeneratePattern": "[a-zA-Z0-9]{50}",
213 | "hidden": true,
214 | "id": "text2504183744",
215 | "max": 60,
216 | "min": 30,
217 | "name": "tokenKey",
218 | "pattern": "",
219 | "presentable": false,
220 | "primaryKey": false,
221 | "required": true,
222 | "system": true,
223 | "type": "text"
224 | },
225 | {
226 | "exceptDomains": null,
227 | "hidden": false,
228 | "id": "email3885137012",
229 | "name": "email",
230 | "onlyDomains": null,
231 | "presentable": false,
232 | "required": true,
233 | "system": true,
234 | "type": "email"
235 | },
236 | {
237 | "hidden": false,
238 | "id": "bool1547992806",
239 | "name": "emailVisibility",
240 | "presentable": false,
241 | "required": false,
242 | "system": true,
243 | "type": "bool"
244 | },
245 | {
246 | "hidden": false,
247 | "id": "bool256245529",
248 | "name": "verified",
249 | "presentable": false,
250 | "required": false,
251 | "system": true,
252 | "type": "bool"
253 | },
254 | {
255 | "autogeneratePattern": "",
256 | "hidden": false,
257 | "id": "text1579384326",
258 | "max": 255,
259 | "min": 0,
260 | "name": "name",
261 | "pattern": "",
262 | "presentable": false,
263 | "primaryKey": false,
264 | "required": false,
265 | "system": false,
266 | "type": "text"
267 | },
268 | {
269 | "hidden": false,
270 | "id": "file376926767",
271 | "maxSelect": 1,
272 | "maxSize": 0,
273 | "mimeTypes": [
274 | "image/jpeg",
275 | "image/png",
276 | "image/svg+xml",
277 | "image/gif",
278 | "image/webp"
279 | ],
280 | "name": "avatar",
281 | "presentable": false,
282 | "protected": false,
283 | "required": false,
284 | "system": false,
285 | "thumbs": null,
286 | "type": "file"
287 | },
288 | {
289 | "hidden": false,
290 | "id": "autodate2990389176",
291 | "name": "created",
292 | "onCreate": true,
293 | "onUpdate": false,
294 | "presentable": false,
295 | "system": false,
296 | "type": "autodate"
297 | },
298 | {
299 | "hidden": false,
300 | "id": "autodate3332085495",
301 | "name": "updated",
302 | "onCreate": true,
303 | "onUpdate": true,
304 | "presentable": false,
305 | "system": false,
306 | "type": "autodate"
307 | }
308 | ],
309 | "indexes": [
310 | "CREATE UNIQUE INDEX `idx_tokenKey__pb_users_auth_` ON `users` (`tokenKey`)",
311 | "CREATE UNIQUE INDEX `idx_email__pb_users_auth_` ON `users` (`email`) WHERE `email` != ''"
312 | ],
313 | "system": false,
314 | "authRule": "",
315 | "manageRule": null,
316 | "authAlert": {
317 | "enabled": true,
318 | "emailTemplate": {
319 | "subject": "Login from a new location",
320 | "body": "Hello,
\nWe noticed a login to your {APP_NAME} account from a new location.
\nIf this was you, you may disregard this email.
\nIf this wasn't you, you should immediately change your {APP_NAME} account password to revoke access from all other locations.
\n\n Thanks, \n {APP_NAME} team\n
"
321 | }
322 | },
323 | "oauth2": {
324 | "mappedFields": {
325 | "id": "",
326 | "name": "name",
327 | "username": "",
328 | "avatarURL": "avatar"
329 | },
330 | "enabled": true
331 | },
332 | "passwordAuth": {
333 | "enabled": true,
334 | "identityFields": [
335 | "email"
336 | ]
337 | },
338 | "mfa": {
339 | "enabled": false,
340 | "duration": 1800,
341 | "rule": ""
342 | },
343 | "otp": {
344 | "enabled": false,
345 | "duration": 180,
346 | "length": 8,
347 | "emailTemplate": {
348 | "subject": "OTP for {APP_NAME}",
349 | "body": "Hello,
\nYour one-time password is: {OTP}
\nIf you didn't ask for the one-time password, you can ignore this email.
\n\n Thanks, \n {APP_NAME} team\n
"
350 | }
351 | },
352 | "authToken": {
353 | "duration": 604800
354 | },
355 | "passwordResetToken": {
356 | "duration": 1800
357 | },
358 | "emailChangeToken": {
359 | "duration": 1800
360 | },
361 | "verificationToken": {
362 | "duration": 259200
363 | },
364 | "fileToken": {
365 | "duration": 180
366 | },
367 | "verificationTemplate": {
368 | "subject": "Verify your {APP_NAME} email",
369 | "body": "Hello,
\nThank you for joining us at {APP_NAME}.
\nClick on the button below to verify your email address.
\n\n Verify \n
\n\n Thanks, \n {APP_NAME} team\n
"
370 | },
371 | "resetPasswordTemplate": {
372 | "subject": "Reset your {APP_NAME} password",
373 | "body": "Hello,
\nClick on the button below to reset your password.
\n\n Reset password \n
\nIf you didn't ask to reset your password, you can ignore this email.
\n\n Thanks, \n {APP_NAME} team\n
"
374 | },
375 | "confirmEmailChangeTemplate": {
376 | "subject": "Confirm your {APP_NAME} new email address",
377 | "body": "Hello,
\nClick on the button below to confirm your new email address.
\n\n Confirm new email \n
\nIf you didn't ask to change your email address, you can ignore this email.
\n\n Thanks, \n {APP_NAME} team\n
"
378 | }
379 | },
380 | {
381 | "id": "pbc_4275539003",
382 | "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId",
383 | "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId",
384 | "createRule": null,
385 | "updateRule": null,
386 | "deleteRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId",
387 | "name": "_authOrigins",
388 | "type": "base",
389 | "fields": [
390 | {
391 | "autogeneratePattern": "[a-z0-9]{15}",
392 | "hidden": false,
393 | "id": "text3208210256",
394 | "max": 15,
395 | "min": 15,
396 | "name": "id",
397 | "pattern": "^[a-z0-9]+$",
398 | "presentable": false,
399 | "primaryKey": true,
400 | "required": true,
401 | "system": true,
402 | "type": "text"
403 | },
404 | {
405 | "autogeneratePattern": "",
406 | "hidden": false,
407 | "id": "text455797646",
408 | "max": 0,
409 | "min": 0,
410 | "name": "collectionRef",
411 | "pattern": "",
412 | "presentable": false,
413 | "primaryKey": false,
414 | "required": true,
415 | "system": true,
416 | "type": "text"
417 | },
418 | {
419 | "autogeneratePattern": "",
420 | "hidden": false,
421 | "id": "text127846527",
422 | "max": 0,
423 | "min": 0,
424 | "name": "recordRef",
425 | "pattern": "",
426 | "presentable": false,
427 | "primaryKey": false,
428 | "required": true,
429 | "system": true,
430 | "type": "text"
431 | },
432 | {
433 | "autogeneratePattern": "",
434 | "hidden": false,
435 | "id": "text4228609354",
436 | "max": 0,
437 | "min": 0,
438 | "name": "fingerprint",
439 | "pattern": "",
440 | "presentable": false,
441 | "primaryKey": false,
442 | "required": true,
443 | "system": true,
444 | "type": "text"
445 | },
446 | {
447 | "hidden": false,
448 | "id": "autodate2990389176",
449 | "name": "created",
450 | "onCreate": true,
451 | "onUpdate": false,
452 | "presentable": false,
453 | "system": true,
454 | "type": "autodate"
455 | },
456 | {
457 | "hidden": false,
458 | "id": "autodate3332085495",
459 | "name": "updated",
460 | "onCreate": true,
461 | "onUpdate": true,
462 | "presentable": false,
463 | "system": true,
464 | "type": "autodate"
465 | }
466 | ],
467 | "indexes": [
468 | "CREATE UNIQUE INDEX `idx_authOrigins_unique_pairs` ON `_authOrigins` (collectionRef, recordRef, fingerprint)"
469 | ],
470 | "system": true
471 | },
472 | {
473 | "id": "pbc_2281828961",
474 | "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId",
475 | "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId",
476 | "createRule": null,
477 | "updateRule": null,
478 | "deleteRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId",
479 | "name": "_externalAuths",
480 | "type": "base",
481 | "fields": [
482 | {
483 | "autogeneratePattern": "[a-z0-9]{15}",
484 | "hidden": false,
485 | "id": "text3208210256",
486 | "max": 15,
487 | "min": 15,
488 | "name": "id",
489 | "pattern": "^[a-z0-9]+$",
490 | "presentable": false,
491 | "primaryKey": true,
492 | "required": true,
493 | "system": true,
494 | "type": "text"
495 | },
496 | {
497 | "autogeneratePattern": "",
498 | "hidden": false,
499 | "id": "text455797646",
500 | "max": 0,
501 | "min": 0,
502 | "name": "collectionRef",
503 | "pattern": "",
504 | "presentable": false,
505 | "primaryKey": false,
506 | "required": true,
507 | "system": true,
508 | "type": "text"
509 | },
510 | {
511 | "autogeneratePattern": "",
512 | "hidden": false,
513 | "id": "text127846527",
514 | "max": 0,
515 | "min": 0,
516 | "name": "recordRef",
517 | "pattern": "",
518 | "presentable": false,
519 | "primaryKey": false,
520 | "required": true,
521 | "system": true,
522 | "type": "text"
523 | },
524 | {
525 | "autogeneratePattern": "",
526 | "hidden": false,
527 | "id": "text2462348188",
528 | "max": 0,
529 | "min": 0,
530 | "name": "provider",
531 | "pattern": "",
532 | "presentable": false,
533 | "primaryKey": false,
534 | "required": true,
535 | "system": true,
536 | "type": "text"
537 | },
538 | {
539 | "autogeneratePattern": "",
540 | "hidden": false,
541 | "id": "text1044722854",
542 | "max": 0,
543 | "min": 0,
544 | "name": "providerId",
545 | "pattern": "",
546 | "presentable": false,
547 | "primaryKey": false,
548 | "required": true,
549 | "system": true,
550 | "type": "text"
551 | },
552 | {
553 | "hidden": false,
554 | "id": "autodate2990389176",
555 | "name": "created",
556 | "onCreate": true,
557 | "onUpdate": false,
558 | "presentable": false,
559 | "system": true,
560 | "type": "autodate"
561 | },
562 | {
563 | "hidden": false,
564 | "id": "autodate3332085495",
565 | "name": "updated",
566 | "onCreate": true,
567 | "onUpdate": true,
568 | "presentable": false,
569 | "system": true,
570 | "type": "autodate"
571 | }
572 | ],
573 | "indexes": [
574 | "CREATE UNIQUE INDEX `idx_externalAuths_record_provider` ON `_externalAuths` (collectionRef, recordRef, provider)",
575 | "CREATE UNIQUE INDEX `idx_externalAuths_collection_provider` ON `_externalAuths` (collectionRef, provider, providerId)"
576 | ],
577 | "system": true
578 | },
579 | {
580 | "id": "pbc_2279338944",
581 | "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId",
582 | "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId",
583 | "createRule": null,
584 | "updateRule": null,
585 | "deleteRule": null,
586 | "name": "_mfas",
587 | "type": "base",
588 | "fields": [
589 | {
590 | "autogeneratePattern": "[a-z0-9]{15}",
591 | "hidden": false,
592 | "id": "text3208210256",
593 | "max": 15,
594 | "min": 15,
595 | "name": "id",
596 | "pattern": "^[a-z0-9]+$",
597 | "presentable": false,
598 | "primaryKey": true,
599 | "required": true,
600 | "system": true,
601 | "type": "text"
602 | },
603 | {
604 | "autogeneratePattern": "",
605 | "hidden": false,
606 | "id": "text455797646",
607 | "max": 0,
608 | "min": 0,
609 | "name": "collectionRef",
610 | "pattern": "",
611 | "presentable": false,
612 | "primaryKey": false,
613 | "required": true,
614 | "system": true,
615 | "type": "text"
616 | },
617 | {
618 | "autogeneratePattern": "",
619 | "hidden": false,
620 | "id": "text127846527",
621 | "max": 0,
622 | "min": 0,
623 | "name": "recordRef",
624 | "pattern": "",
625 | "presentable": false,
626 | "primaryKey": false,
627 | "required": true,
628 | "system": true,
629 | "type": "text"
630 | },
631 | {
632 | "autogeneratePattern": "",
633 | "hidden": false,
634 | "id": "text1582905952",
635 | "max": 0,
636 | "min": 0,
637 | "name": "method",
638 | "pattern": "",
639 | "presentable": false,
640 | "primaryKey": false,
641 | "required": true,
642 | "system": true,
643 | "type": "text"
644 | },
645 | {
646 | "hidden": false,
647 | "id": "autodate2990389176",
648 | "name": "created",
649 | "onCreate": true,
650 | "onUpdate": false,
651 | "presentable": false,
652 | "system": true,
653 | "type": "autodate"
654 | },
655 | {
656 | "hidden": false,
657 | "id": "autodate3332085495",
658 | "name": "updated",
659 | "onCreate": true,
660 | "onUpdate": true,
661 | "presentable": false,
662 | "system": true,
663 | "type": "autodate"
664 | }
665 | ],
666 | "indexes": [
667 | "CREATE INDEX `idx_mfas_collectionRef_recordRef` ON `_mfas` (collectionRef,recordRef)"
668 | ],
669 | "system": true
670 | },
671 | {
672 | "id": "pbc_1638494021",
673 | "listRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId",
674 | "viewRule": "@request.auth.id != '' && recordRef = @request.auth.id && collectionRef = @request.auth.collectionId",
675 | "createRule": null,
676 | "updateRule": null,
677 | "deleteRule": null,
678 | "name": "_otps",
679 | "type": "base",
680 | "fields": [
681 | {
682 | "autogeneratePattern": "[a-z0-9]{15}",
683 | "hidden": false,
684 | "id": "text3208210256",
685 | "max": 15,
686 | "min": 15,
687 | "name": "id",
688 | "pattern": "^[a-z0-9]+$",
689 | "presentable": false,
690 | "primaryKey": true,
691 | "required": true,
692 | "system": true,
693 | "type": "text"
694 | },
695 | {
696 | "autogeneratePattern": "",
697 | "hidden": false,
698 | "id": "text455797646",
699 | "max": 0,
700 | "min": 0,
701 | "name": "collectionRef",
702 | "pattern": "",
703 | "presentable": false,
704 | "primaryKey": false,
705 | "required": true,
706 | "system": true,
707 | "type": "text"
708 | },
709 | {
710 | "autogeneratePattern": "",
711 | "hidden": false,
712 | "id": "text127846527",
713 | "max": 0,
714 | "min": 0,
715 | "name": "recordRef",
716 | "pattern": "",
717 | "presentable": false,
718 | "primaryKey": false,
719 | "required": true,
720 | "system": true,
721 | "type": "text"
722 | },
723 | {
724 | "cost": 8,
725 | "hidden": true,
726 | "id": "password901924565",
727 | "max": 0,
728 | "min": 0,
729 | "name": "password",
730 | "pattern": "",
731 | "presentable": false,
732 | "required": true,
733 | "system": true,
734 | "type": "password"
735 | },
736 | {
737 | "autogeneratePattern": "",
738 | "hidden": true,
739 | "id": "text3866985172",
740 | "max": 0,
741 | "min": 0,
742 | "name": "sentTo",
743 | "pattern": "",
744 | "presentable": false,
745 | "primaryKey": false,
746 | "required": false,
747 | "system": true,
748 | "type": "text"
749 | },
750 | {
751 | "hidden": false,
752 | "id": "autodate2990389176",
753 | "name": "created",
754 | "onCreate": true,
755 | "onUpdate": false,
756 | "presentable": false,
757 | "system": true,
758 | "type": "autodate"
759 | },
760 | {
761 | "hidden": false,
762 | "id": "autodate3332085495",
763 | "name": "updated",
764 | "onCreate": true,
765 | "onUpdate": true,
766 | "presentable": false,
767 | "system": true,
768 | "type": "autodate"
769 | }
770 | ],
771 | "indexes": [
772 | "CREATE INDEX `idx_otps_collectionRef_recordRef` ON `_otps` (collectionRef, recordRef)"
773 | ],
774 | "system": true
775 | },
776 | {
777 | "id": "pbc_3552922951",
778 | "listRule": "",
779 | "viewRule": null,
780 | "createRule": null,
781 | "updateRule": null,
782 | "deleteRule": null,
783 | "name": "models",
784 | "type": "base",
785 | "fields": [
786 | {
787 | "autogeneratePattern": "[a-z0-9]{15}",
788 | "hidden": false,
789 | "id": "text3208210256",
790 | "max": 15,
791 | "min": 15,
792 | "name": "id",
793 | "pattern": "^[a-z0-9]+$",
794 | "presentable": false,
795 | "primaryKey": true,
796 | "required": true,
797 | "system": true,
798 | "type": "text"
799 | },
800 | {
801 | "autogeneratePattern": "",
802 | "hidden": false,
803 | "id": "text1579384326",
804 | "max": 0,
805 | "min": 0,
806 | "name": "name",
807 | "pattern": "",
808 | "presentable": false,
809 | "primaryKey": false,
810 | "required": false,
811 | "system": false,
812 | "type": "text"
813 | },
814 | {
815 | "hidden": false,
816 | "id": "autodate2990389176",
817 | "name": "created",
818 | "onCreate": true,
819 | "onUpdate": false,
820 | "presentable": false,
821 | "system": false,
822 | "type": "autodate"
823 | },
824 | {
825 | "hidden": false,
826 | "id": "autodate3332085495",
827 | "name": "updated",
828 | "onCreate": true,
829 | "onUpdate": true,
830 | "presentable": false,
831 | "system": false,
832 | "type": "autodate"
833 | }
834 | ],
835 | "indexes": [
836 | "CREATE UNIQUE INDEX `idx_33TuWqay3q` ON `models` (`name`)"
837 | ],
838 | "system": false
839 | }
840 | ]
--------------------------------------------------------------------------------