feat: add login page (#93)

* feat: add signIn

* feat: add connect service

* feat: add login page
This commit is contained in:
BiggerRain
2025-01-08 16:51:05 +08:00
committed by GitHub
parent 1f032139b2
commit 6e39e25d72
22 changed files with 566 additions and 99 deletions

View File

@@ -8,6 +8,7 @@
"dyld",
"fullscreen",
"headlessui",
"Icdbb",
"icns",
"INFINI",
"inputbox",

View File

@@ -11,6 +11,7 @@
},
"dependencies": {
"@headlessui/react": "^2.1.10",
"@react-oauth/google": "^0.12.1",
"@tauri-apps/api": ">=2.0.0",
"@tauri-apps/plugin-autostart": "~2",
"@tauri-apps/plugin-global-shortcut": "~2.0.0",

14
pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@headlessui/react':
specifier: ^2.1.10
version: 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-oauth/google':
specifier: ^0.12.1
version: 0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@tauri-apps/api':
specifier: '>=2.0.0'
version: 2.0.2
@@ -501,6 +504,12 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-oauth/google@0.12.1':
resolution: {integrity: sha512-qagsy22t+7UdkYAiT5ZhfM4StXi9PPNvw0zuwNmabrWyMKddczMtBIOARflbaIj+wHiQjnMAsZmzsUYuXeyoSg==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@react-stately/utils@3.10.4':
resolution: {integrity: sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==}
peerDependencies:
@@ -2602,6 +2611,11 @@ snapshots:
clsx: 2.1.1
react: 18.3.1
'@react-oauth/google@0.12.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@react-stately/utils@3.10.4(react@18.3.1)':
dependencies:
'@swc/helpers': 0.5.13

View File

@@ -28,7 +28,7 @@
"core:window:allow-get-all-windows",
"core:window:allow-set-focus",
"core:app:allow-set-app-theme",
"shell:allow-open",
"shell:default",
"http:default",
"http:allow-fetch",
"http:allow-fetch-cancel",

BIN
src/assets/images/apple.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -5,6 +5,8 @@ import {
CornerDownLeft,
} from "lucide-react";
import logoImg from "@/assets/32x32.png";
interface FooterProps {
isChat: boolean;
name?: string;
@@ -17,6 +19,13 @@ export default function Footer({ name }: FooterProps) {
className="px-4 z-999 mx-[1px] h-10 absolute bottom-0 left-0 right-0 border-t border-gray-200 dark:border-gray-700 flex items-center justify-between rounded-xl rounded-t-none overflow-hidden"
>
<div className="flex items-center">
<div className="flex items-center space-x-4">
<img src={logoImg} className="w-5 h-5" />
<span className="text-xs text-gray-500 dark:text-gray-400">
Version 1.0.0
</span>
</div>
{name ? (
<div className="flex gap-2 items-center text-[#666] text-xs">
<AppWindowMac className="w-5 h-5" /> {name}

View File

@@ -1,52 +1,70 @@
import { useState } from "react";
import { Cloud } from "lucide-react";
import { UserProfile } from "./UserProfile";
import { DataSourcesList } from "./DataSourcesList";
import { Sidebar } from "./Sidebar";
import { ConnectService } from "./ConnectService";
export default function CocoCloud() {
return (
<div className="min-h-screen bg-gray-50">
<div className="max-w-4xl mx-auto px-4 py-8">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2 px-4 py-2 bg-white rounded-md border border-gray-200">
<Cloud className="w-5 h-5 text-blue-500" />
<span className="font-medium">Coco Cloud</span>
</div>
<span className="px-3 py-1 text-sm text-blue-600 bg-blue-50 rounded-md">
Available
</span>
</div>
<button className="p-2 text-gray-500 hover:text-gray-700">
<Cloud className="w-5 h-5" />
</button>
</div>
<div className="mb-8">
<div className="text-sm text-gray-500 mb-4">
<span>Service provision: INFINI Labs</span>
<span className="mx-4">|</span>
<span>Version Number: v2.3.0</span>
<span className="mx-4">|</span>
<span>Update time: 2023-05-12</span>
</div>
<p className="text-gray-600 leading-relaxed">
Coco Cloud provides users with a cloud storage and data integration
platform that supports account registration and data source
management. Users can integrate multiple data sources (such as
Google Drive, yuque, GitHub, etc.), easily access and search for
files, documents and codes across platforms, and achieve efficient
data collaboration and management.
</p>
</div>
<div>
<h2 className="text-lg font-medium text-gray-900 mb-4">Account Information</h2>
<button className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors">
Login
</button>
</div>
const [isLogin, setIsLogin] = useState(true);
const [isConnect, setIsConnect] = useState(true);
<UserProfile name="Rain" email="an121245@gmail.com" />
<DataSourcesList />
</div>
return (
<div className="flex min-h-screen bg-gray-50">
<Sidebar />
<main className="flex-1">
{isConnect ? <div className="max-w-4xl mx-auto px-4 py-8">
<div className="flex items-center justify-between mb-8">
<div className="flex items-center space-x-3">
<div className="flex items-center space-x-2 px-4 py-2 bg-white rounded-md border border-gray-200">
<Cloud className="w-5 h-5 text-blue-500" />
<span className="font-medium">Coco Cloud</span>
</div>
<span className="px-3 py-1 text-sm text-blue-600 bg-blue-50 rounded-md">
Available
</span>
</div>
<button className="p-2 text-gray-500 hover:text-gray-700">
<Cloud className="w-5 h-5" />
</button>
</div>
<div className="mb-8">
<div className="text-sm text-gray-500 mb-4">
<span>Service provision: INFINI Labs</span>
<span className="mx-4">|</span>
<span>Version Number: v2.3.0</span>
<span className="mx-4">|</span>
<span>Update time: 2023-05-12</span>
</div>
<p className="text-gray-600 leading-relaxed">
Coco Cloud provides users with a cloud storage and data
integration platform that supports account registration and data
source management. Users can integrate multiple data sources (such
as Google Drive, yuque, GitHub, etc.), easily access and search
for files, documents and codes across platforms, and achieve
efficient data collaboration and management.
</p>
</div>
<div className="mb-8">
<h2 className="text-lg font-medium text-gray-900 mb-4">
Account Information
</h2>
{isLogin ? (
<UserProfile name="Rain" email="an121245@gmail.com" />
) : (
<button className="px-6 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 transition-colors">
Login
</button>
)}
</div>
{isLogin ? <DataSourcesList /> : null }
</div>: <ConnectService />}
</main>
</div>
);
}

View File

@@ -0,0 +1,53 @@
import React, { useState } from 'react';
import { ArrowLeft } from 'lucide-react';
export function ConnectService() {
const [sourceName, setSourceName] = useState('');
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
console.log('Connecting Google Drive with name:', sourceName);
};
return (
<div className="p-8 max-w-4xl">
<div className="mb-8">
<button className="flex items-center text-gray-600 hover:text-gray-900">
<ArrowLeft className="w-5 h-5 mr-2" />
<span>Connect Google Drive</span>
</button>
</div>
<div className="mb-8">
<p className="text-gray-600">
Coco needs to obtain authorization from your Google Drive account
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label htmlFor="sourceName" className="block text-sm font-medium text-gray-700 mb-1">
Data Source Name
</label>
<input
type="text"
id="sourceName"
value={sourceName}
onChange={(e) => setSourceName(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
placeholder="Your Google Drive"
/>
</div>
<div className="flex justify-end">
<button
type="submit"
className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors"
>
Save
</button>
</div>
</form>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { Link2, Trash2 } from 'lucide-react';
import { Link2, Trash2 } from "lucide-react";
interface Account {
email: string;
@@ -18,18 +18,16 @@ export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) {
<div className="border border-gray-200 rounded-lg p-4">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center space-x-3">
<img
src={`/icons/${type}.svg`}
alt={name}
className="w-6 h-6"
/>
<img src={`/icons/${type}.svg`} alt={name} className="w-6 h-6" />
<span className="font-medium">{name}</span>
</div>
<button className="text-blue-500 hover:text-blue-600 flex items-center space-x-1">
<Link2 className="w-4 h-4" />
<span className="text-sm">{isConnected ? '管理' : '连接账户'}</span>
</button>
</div>
<div className="text-sm text-gray-500 mb-2">
{isConnected ? "Manage" : "Connect Accounts"}
</div>
{accounts.map((account, index) => (
<div
@@ -44,14 +42,14 @@ export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) {
</div>
<div>
<div className="text-sm font-medium">
{index === 0 ? '我的网盘' : `网盘${index + 1}`}
{index === 0 ? "My network disk" : `Network disk ${index + 1}`}
</div>
<div className="text-xs text-gray-500">{account.email}</div>
</div>
</div>
<div className="flex items-center space-x-4">
<span className="text-xs text-gray-500">
: {account.lastSync}
Recently Synced: {account.lastSync}
</span>
<button className="text-gray-400 hover:text-gray-600">
<Trash2 className="w-4 h-4" />

View File

@@ -7,8 +7,8 @@ export function DataSourcesList() {
name: 'Google Drive',
type: 'google',
accounts: [
{ email: 'an121245@gmail.com', lastSync: '2025年1月2日 09:50 AM' },
{ email: '9paiii@gmail.com', lastSync: '2025年1月2日 09:50 AM' }
{ email: 'an121245@gmail.com', lastSync: '2025-01-02 09:50 AM' },
{ email: '9paiii@gmail.com', lastSync: '2025-01-02 09:50 AM' }
]
},
{
@@ -27,7 +27,7 @@ export function DataSourcesList() {
return (
<div className="space-y-4">
<h2 className="text-xl font-medium text-gray-900"></h2>
<h2 className="text-xl font-medium text-gray-900">Data Source</h2>
<div className="space-y-4">
{dataSources.map(source => (
<DataSourceItem key={source.id} {...source} />

View File

@@ -0,0 +1,28 @@
import { Cloud, Plus } from "lucide-react";
export function Sidebar() {
return (
<div className="w-64 border-r border-gray-200 bg-white">
<div className="p-4">
<div className="flex items-center space-x-2 px-3 py-2 bg-blue-50 text-blue-600 rounded-lg mb-6">
<Cloud className="w-5 h-5" />
<span className="font-medium">Coco Cloud</span>
<div className="flex-1" />
<button className="text-blue-600 hover:text-blue-700">
<Cloud className="w-4 h-4" />
</button>
</div>
<div className="space-y-2">
<div className="text-sm font-medium text-gray-500 px-3 mb-2">
Third-party services
</div>
<button className="w-full flex items-center justify-center p-2 border-2 border-dashed border-gray-200 rounded-lg text-gray-400 hover:text-gray-600 hover:border-gray-300">
<Plus className="w-5 h-5" />
</button>
</div>
</div>
</div>
);
}

View File

@@ -8,7 +8,6 @@ interface UserProfileProps {
export function UserProfile({ name, email }: UserProfileProps) {
return (
<div className="space-y-6">
<h2 className="text-xl font-medium text-gray-900"></h2>
<div className="flex items-center space-x-4">
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center">
<User className="w-6 h-6 text-gray-500" />

View File

@@ -0,0 +1,59 @@
export default `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Cache-Control" content="no-store, no-cache, must-revalidate">
<meta http-equiv="Pragma" content="no-cache">
<title>Coco Auth</title>
<style>
html, body {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
font-weight: 400;
}
body {
display: flex;
align-items: center;
justify-content: center;
font-family: sans-serif;
text-align: center;
background-color: #f8f9fa;
}
.container {
padding: 30px;
width: 100%;
max-width: 400px;
margin: 0 auto;
}
.logo {
width: 130px;
height: auto;
margin-bottom: 20px;
}
p {
font-size: 21px;
line-height: 26px;
color: #12161F;
margin: 0;
}
.error {
color: #dc2626;
margin-top: 12px;
font-size: 16px;
}
</style>
</head>
<body>
<div class="container">
<h1>Coco<h1>
<p id="message">You are now signed in. Please re-open the Coco desktop app to continue.</p>
<div id="error-container"></div>
</div>
</body>
</html>
`;

View File

@@ -0,0 +1,55 @@
import { useEffect } from 'react';
import { Github, Mail, Apple } from 'lucide-react';
import { useSearchParams } from "react-router-dom";
import { LoginForm } from '@/components/Auth/LoginForm';
import { SocialButton } from '@/components/Auth/SocialButton';
import { Divider } from '@/components/Auth/Divider';
import { authWitheGithub } from '@/utils/index';
export default function LoginPage() {
const [searchParams] = useSearchParams();
const uid = searchParams.get("uid");
const code = searchParams.get("code");
useEffect(()=>{
}, [code])
function GithubClick() {
uid && authWitheGithub(uid)
}
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-gray-800 rounded-xl p-8 shadow-2xl">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-white mb-2">Welcome Back</h1>
<p className="text-gray-400">Sign in to continue to Coco</p>
</div>
<div className="space-y-4 mb-6">
<SocialButton
icon={<Github className="w-5 h-5" />}
provider="GitHub"
onClick={() => GithubClick()}
/>
<SocialButton
icon={<Mail className="w-5 h-5" />}
provider="Google"
onClick={() => console.log('Google login')}
/>
<SocialButton
icon={<Apple className="w-5 h-5" />}
provider="Apple"
onClick={() => console.log('Apple login')}
/>
</div>
<Divider />
<LoginForm onSubmit={(email, password) => console.log(email, password)} />
</div>
</div>
);
}

View File

@@ -1,13 +1,133 @@
import { useState, useEffect } from "react";
import { useNavigate } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
import { invoke } from "@tauri-apps/api/core";
import { listen } from "@tauri-apps/api/event";
import { getCurrentWindow } from "@tauri-apps/api/window";
import * as shell from "@tauri-apps/plugin-shell";
import { useAppStore } from "@/stores/appStore";
import { useAuthStore } from "@/stores/authStore";
import { OpenBrowserURL } from "@/utils/index";
import logoImg from "@/assets/32x32.png";
import callbackTemplate from "@/components/Auth/callback.template";
import { clientEnv } from "@/utils/env";
export default function Account() {
const app_uid = useAppStore((state) => state.app_uid);
const setAppUid = useAppStore((state) => state.setAppUid);
const { auth, setAuth } = useAuthStore();
const navigate = useNavigate();
const [loading, setLoading] = useState(false);
useEffect(() => {
let unsubscribe: (() => void) | undefined;
const setupAuthListener = async () => {
try {
if (!auth) {
// Replace the current route with signin
// navigate("/signin", { replace: true });
}
} catch (error) {
console.error("Failed to set up auth listener:", error);
}
};
setupAuthListener();
// Clean up logic on unmount
return () => {
const cleanup = async () => {
try {
await invoke("plugin:oauth|stop");
} catch (e) {
// Ignore errors if no server is running
}
if (unsubscribe) {
unsubscribe();
}
};
cleanup();
};
}, [auth]);
async function signIn() {
let res: (url: URL) => void;
try {
const stopListening = await listen(
"oauth://url",
(data: { payload: string }) => {
if (!data.payload.includes("token")) {
return;
}
const urlObject = new URL(data.payload);
res(urlObject);
}
);
// Stop any existing OAuth server first
try {
await invoke("plugin:oauth|stop");
} catch (e) {
// Ignore errors if no server is running
}
const port: string = await invoke("plugin:oauth|start", {
config: {
response: callbackTemplate,
headers: {
"Content-Type": "text/html; charset=utf-8",
"Cache-Control": "no-store, no-cache, must-revalidate",
Pragma: "no-cache",
},
// Add a cleanup function to stop the server after handling the request
cleanup: true,
},
});
await shell.open(
`${clientEnv.COCO_SERVER_URL}/api/desktop/session/request?port=${port}`
);
const url = await new Promise<URL>((r) => {
res = r;
});
stopListening();
const token = url.searchParams.get("token");
const user_id = url.searchParams.get("user_id");
const expires = Number(url.searchParams.get("expires"));
if (!token || !expires || !user_id) {
throw new Error("Invalid token or expires");
}
await setAuth({
token,
user_id,
expires,
plan: { upgraded: false, last_checked: 0 },
});
getCurrentWindow()
.setFocus()
.catch(() => {});
return navigate("/");
} catch (error) {
console.error("Sign in failed:", error);
await setAuth(undefined);
throw error;
}
}
async function initializeUser() {
let uid = app_uid;
if (!uid) {
@@ -22,6 +142,15 @@ export default function Account() {
// const { token } = await response.json();
// localStorage.setItem("auth_token", token);
OpenBrowserURL(`http://localhost:1420/login?uid=${uid}`);
setLoading(true);
try {
await signIn();
} catch (error) {
console.error(error);
} finally {
setLoading(false);
}
}
function LoginClick(event: React.MouseEvent<HTMLAnchorElement, MouseEvent>) {
@@ -59,7 +188,7 @@ export default function Account() {
className="text-sm/6 font-semibold text-gray-900 dark:text-gray-100"
onClick={LoginClick}
>
Log In <span aria-hidden="true"></span>
{loading ? "Signing In..." : "Sign In"}
</a>
</div>
</div>

View File

@@ -7,7 +7,7 @@ import SettingsPanel from "./SettingsPanel";
import GeneralSettings from "./GeneralSettings";
import AboutView from "./AboutView";
import Account from "./Account";
// import CocoCloud from "@/components/Auth/CocoCloud"
import CocoCloud from "@/components/Auth/CocoCloud"
import Footer from "../Footer";
import { useTheme } from "../../contexts/ThemeContext";
import { AppTheme } from "../../utils/tauri";
@@ -25,7 +25,7 @@ function SettingsPage() {
const tabs = [
{ name: "General", icon: Settings },
{ name: "Extensions", icon: Puzzle },
{ name: "Account", icon: User },
{ name: "Connect", icon: User },
{ name: "Advanced", icon: Settings2 },
{ name: "About", icon: Info },
];
@@ -79,7 +79,7 @@ function SettingsPage() {
</TabPanel>
<TabPanel>
<Account />
{/* <CocoCloud /> */}
<CocoCloud />
</TabPanel>
<TabPanel>
<SettingsPanel title="">

View File

@@ -1,54 +1,123 @@
import { Github, Mail, Apple } from 'lucide-react';
import { useEffect } from "react";
import { GoogleOAuthProvider } from "@react-oauth/google";
import { useSearchParams } from "react-router-dom";
import { LoginForm } from '@/components/Auth/LoginForm';
import { SocialButton } from '@/components/Auth/SocialButton';
import { Divider } from '@/components/Auth/Divider';
import { authWitheGithub } from '@/utils/index';
import { useEffect } from 'react';
import { authWitheGithub } from "@/utils/index";
import loginImg from "@/assets/images/bg-login.png";
import logoImg from "@/assets/images/coco-logo.png";
import AppleImg from "@/assets/images/apple.png";
import GithubImg from "@/assets/images/github.png";
import GoogleImg from "@/assets/images/google.png";
export default function LoginPage() {
const handleGoogleSignIn = (response: any) => {
console.log("Google Login Success:", response);
// response.credential
};
const [searchParams] = useSearchParams();
const uid = searchParams.get("uid");
const code = searchParams.get("code");
useEffect(()=>{
useEffect(() => {}, [code]);
}, [code])
function GithubClick() {
uid && authWitheGithub(uid)
function handleGithubSignIn() {
uid && authWitheGithub(uid);
}
const clientId = "YOUR_APPLE_CLIENT_ID";
const redirectUri = "http://localhost:3000";
const scope = "name email";
const handleAppleSignIn = () => {
const authUrl = `https://appleid.apple.com/auth/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&response_type=code`;
window.location.href = authUrl;
};
return (
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
<div className="w-full max-w-md bg-gray-800 rounded-xl p-8 shadow-2xl">
<div className="text-center mb-8">
<h1 className="text-2xl font-bold text-white mb-2">Welcome Back</h1>
<p className="text-gray-400">Sign in to continue to Coco</p>
<div className="min-h-screen bg-black flex justify-center">
<div className="min-h-screen container py-[30px] relative overflow-hidden">
{/* Background Image */}
<div className="absolute top-[60px] inset-0 z-0">
<img
src={loginImg}
alt="Background"
className="w-full h-full object-cover opacity-50"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/80 to-black/20" />
</div>
<div className="space-y-4 mb-6">
<SocialButton
icon={<Github className="w-5 h-5" />}
provider="GitHub"
onClick={() => GithubClick()}
/>
<SocialButton
icon={<Mail className="w-5 h-5" />}
provider="Google"
onClick={() => console.log('Google login')}
/>
<SocialButton
icon={<Apple className="w-5 h-5" />}
provider="Apple"
onClick={() => console.log('Apple login')}
/>
{/* Content */}
<div className="relative z-10 w-full max-w-[100%] px-[130px]">
{/* Logo */}
<div className="flex items-center mb-[60px]">
<img src={logoImg} alt="Coco" className="h-10 w-[127px]" />
</div>
{/* Main Text */}
<div className="text-left mb-[60px]">
<h1 className="text-[64px] leading-[72px] font-bold text-white mb-4">
INSERT
<br />
THE
<br />
STRAW
</h1>
<h2 className="text-[64px] leading-[72px] font-bold text-cyan-400 mb-6">
LET'S BEGIN!
</h2>
<p className="text-white">
With Coco AI, accessing your data is as easy as sipping fresh
coconut juice.
</p>
</div>
{/* Social Login Buttons */}
<div className="font-bold text-[20px] text-white leading-[30px] text-left uppercase mb-5">
Sign in With
</div>
<div className="flex gap-8">
<GoogleOAuthProvider clientId="YOUR_GOOGLE_CLIENT_ID">
<button
className="w-[60px] h-[60px] bg-white hover:bg-gray-100 text-black rounded-full py-3 px-4 flex items-center justify-center space-x-2 transition-colors"
onClick={handleGoogleSignIn}
>
<img
src={GoogleImg}
alt="Continue with Google"
className="w-[29px] h-[29px]"
/>
</button>
</GoogleOAuthProvider>
<button
className="w-[60px] h-[60px] bg-white hover:bg-gray-100 text-black rounded-full py-3 px-4 flex items-center justify-center space-x-2 transition-colors"
onClick={handleGithubSignIn}
>
<img
src={AppleImg}
alt="Continue with Apple"
className="w-[26px] h-[30px]"
/>
</button>
<button
className="w-[60px] h-[60px] bg-white hover:bg-gray-100 text-black rounded-full py-3 px-4 flex items-center justify-center space-x-2 transition-colors"
onClick={handleAppleSignIn}
>
<img
src={GithubImg}
alt="Continue with GitHub"
className="w-[30px] h-[29px]"
/>
</button>
</div>
</div>
<Divider />
<LoginForm onSubmit={(email, password) => console.log(email, password)} />
{/* Footer */}
<div className="absolute bottom-8 w-full text-sm text-gray-500 flex justify-center">
© {new Date().getFullYear()} Coco Labs. All rights reserved.
</div>
</div>
</div>
);

34
src/stores/authStore.ts Normal file
View File

@@ -0,0 +1,34 @@
import { create } from "zustand";
import { persist } from "zustand/middleware";
export type Plan = {
upgraded: boolean;
last_checked: number;
};
export type AuthStore = {
token: string;
user_id: string | null;
expires: number;
plan: Plan | null;
};
export type IAuthStore = {
auth: AuthStore | undefined;
setAuth: (auth: AuthStore | undefined) => void;
resetAuth: () => void;
};
export const useAuthStore = create<IAuthStore>()(
persist(
(set) => ({
auth: undefined,
setAuth: (auth) => set({ auth }),
resetAuth: () => set({ auth: undefined }),
}),
{
name: "auth-store",
partialize: (state) => ({ auth: state.auth }),
}
)
);