mirror of
https://github.com/infinilabs/coco-app.git
synced 2025-12-16 11:37:47 +01:00
feat: add login page (#93)
* feat: add signIn * feat: add connect service * feat: add login page
This commit is contained in:
1
.vscode/settings.json
vendored
1
.vscode/settings.json
vendored
@@ -8,6 +8,7 @@
|
|||||||
"dyld",
|
"dyld",
|
||||||
"fullscreen",
|
"fullscreen",
|
||||||
"headlessui",
|
"headlessui",
|
||||||
|
"Icdbb",
|
||||||
"icns",
|
"icns",
|
||||||
"INFINI",
|
"INFINI",
|
||||||
"inputbox",
|
"inputbox",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@headlessui/react": "^2.1.10",
|
"@headlessui/react": "^2.1.10",
|
||||||
|
"@react-oauth/google": "^0.12.1",
|
||||||
"@tauri-apps/api": ">=2.0.0",
|
"@tauri-apps/api": ">=2.0.0",
|
||||||
"@tauri-apps/plugin-autostart": "~2",
|
"@tauri-apps/plugin-autostart": "~2",
|
||||||
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
|
"@tauri-apps/plugin-global-shortcut": "~2.0.0",
|
||||||
|
|||||||
14
pnpm-lock.yaml
generated
14
pnpm-lock.yaml
generated
@@ -11,6 +11,9 @@ importers:
|
|||||||
'@headlessui/react':
|
'@headlessui/react':
|
||||||
specifier: ^2.1.10
|
specifier: ^2.1.10
|
||||||
version: 2.1.10(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
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':
|
'@tauri-apps/api':
|
||||||
specifier: '>=2.0.0'
|
specifier: '>=2.0.0'
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
@@ -501,6 +504,12 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
|
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':
|
'@react-stately/utils@3.10.4':
|
||||||
resolution: {integrity: sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==}
|
resolution: {integrity: sha512-gBEQEIMRh5f60KCm7QKQ2WfvhB2gLUr9b72sqUdIZ2EG+xuPgaIlCBeSicvjmjBvYZwOjoOEnmIkcx2GHp/HWw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2602,6 +2611,11 @@ snapshots:
|
|||||||
clsx: 2.1.1
|
clsx: 2.1.1
|
||||||
react: 18.3.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)':
|
'@react-stately/utils@3.10.4(react@18.3.1)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@swc/helpers': 0.5.13
|
'@swc/helpers': 0.5.13
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"core:window:allow-get-all-windows",
|
"core:window:allow-get-all-windows",
|
||||||
"core:window:allow-set-focus",
|
"core:window:allow-set-focus",
|
||||||
"core:app:allow-set-app-theme",
|
"core:app:allow-set-app-theme",
|
||||||
"shell:allow-open",
|
"shell:default",
|
||||||
"http:default",
|
"http:default",
|
||||||
"http:allow-fetch",
|
"http:allow-fetch",
|
||||||
"http:allow-fetch-cancel",
|
"http:allow-fetch-cancel",
|
||||||
|
|||||||
BIN
src/assets/images/apple.png
Normal file
BIN
src/assets/images/apple.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.3 KiB |
BIN
src/assets/images/bg-login.png
Normal file
BIN
src/assets/images/bg-login.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
BIN
src/assets/images/coco-logo.png
Normal file
BIN
src/assets/images/coco-logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
src/assets/images/github.png
Normal file
BIN
src/assets/images/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
BIN
src/assets/images/google.png
Normal file
BIN
src/assets/images/google.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.2 KiB |
@@ -5,6 +5,8 @@ import {
|
|||||||
CornerDownLeft,
|
CornerDownLeft,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
|
import logoImg from "@/assets/32x32.png";
|
||||||
|
|
||||||
interface FooterProps {
|
interface FooterProps {
|
||||||
isChat: boolean;
|
isChat: boolean;
|
||||||
name?: string;
|
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"
|
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">
|
||||||
|
<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 ? (
|
{name ? (
|
||||||
<div className="flex gap-2 items-center text-[#666] text-xs">
|
<div className="flex gap-2 items-center text-[#666] text-xs">
|
||||||
<AppWindowMac className="w-5 h-5" /> {name}
|
<AppWindowMac className="w-5 h-5" /> {name}
|
||||||
|
|||||||
@@ -1,52 +1,70 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { Cloud } from "lucide-react";
|
import { Cloud } from "lucide-react";
|
||||||
|
|
||||||
import { UserProfile } from "./UserProfile";
|
import { UserProfile } from "./UserProfile";
|
||||||
import { DataSourcesList } from "./DataSourcesList";
|
import { DataSourcesList } from "./DataSourcesList";
|
||||||
|
import { Sidebar } from "./Sidebar";
|
||||||
|
import { ConnectService } from "./ConnectService";
|
||||||
|
|
||||||
export default function CocoCloud() {
|
export default function CocoCloud() {
|
||||||
return (
|
const [isLogin, setIsLogin] = useState(true);
|
||||||
<div className="min-h-screen bg-gray-50">
|
const [isConnect, setIsConnect] = useState(true);
|
||||||
<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>
|
|
||||||
|
|
||||||
<UserProfile name="Rain" email="an121245@gmail.com" />
|
return (
|
||||||
<DataSourcesList />
|
<div className="flex min-h-screen bg-gray-50">
|
||||||
</div>
|
<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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
53
src/components/Auth/ConnectService.tsx
Normal file
53
src/components/Auth/ConnectService.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Link2, Trash2 } from 'lucide-react';
|
import { Link2, Trash2 } from "lucide-react";
|
||||||
|
|
||||||
interface Account {
|
interface Account {
|
||||||
email: string;
|
email: string;
|
||||||
@@ -18,21 +18,19 @@ export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) {
|
|||||||
<div className="border border-gray-200 rounded-lg p-4">
|
<div className="border border-gray-200 rounded-lg p-4">
|
||||||
<div className="flex items-center justify-between mb-4">
|
<div className="flex items-center justify-between mb-4">
|
||||||
<div className="flex items-center space-x-3">
|
<div className="flex items-center space-x-3">
|
||||||
<img
|
<img src={`/icons/${type}.svg`} alt={name} className="w-6 h-6" />
|
||||||
src={`/icons/${type}.svg`}
|
|
||||||
alt={name}
|
|
||||||
className="w-6 h-6"
|
|
||||||
/>
|
|
||||||
<span className="font-medium">{name}</span>
|
<span className="font-medium">{name}</span>
|
||||||
</div>
|
</div>
|
||||||
<button className="text-blue-500 hover:text-blue-600 flex items-center space-x-1">
|
<button className="text-blue-500 hover:text-blue-600 flex items-center space-x-1">
|
||||||
<Link2 className="w-4 h-4" />
|
<Link2 className="w-4 h-4" />
|
||||||
<span className="text-sm">{isConnected ? '管理' : '连接账户'}</span>
|
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="text-sm text-gray-500 mb-2">
|
||||||
|
{isConnected ? "Manage" : "Connect Accounts"}
|
||||||
|
</div>
|
||||||
|
|
||||||
{accounts.map((account, index) => (
|
{accounts.map((account, index) => (
|
||||||
<div
|
<div
|
||||||
key={account.email}
|
key={account.email}
|
||||||
className="flex items-center justify-between py-2 border-t border-gray-100"
|
className="flex items-center justify-between py-2 border-t border-gray-100"
|
||||||
>
|
>
|
||||||
@@ -44,14 +42,14 @@ export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm font-medium">
|
<div className="text-sm font-medium">
|
||||||
{index === 0 ? '我的网盘' : `网盘${index + 1}`}
|
{index === 0 ? "My network disk" : `Network disk ${index + 1}`}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-gray-500">{account.email}</div>
|
<div className="text-xs text-gray-500">{account.email}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center space-x-4">
|
<div className="flex items-center space-x-4">
|
||||||
<span className="text-xs text-gray-500">
|
<span className="text-xs text-gray-500">
|
||||||
最近同步: {account.lastSync}
|
Recently Synced: {account.lastSync}
|
||||||
</span>
|
</span>
|
||||||
<button className="text-gray-400 hover:text-gray-600">
|
<button className="text-gray-400 hover:text-gray-600">
|
||||||
<Trash2 className="w-4 h-4" />
|
<Trash2 className="w-4 h-4" />
|
||||||
@@ -61,4 +59,4 @@ export function DataSourceItem({ name, type, accounts }: DataSourceItemProps) {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export function DataSourcesList() {
|
|||||||
name: 'Google Drive',
|
name: 'Google Drive',
|
||||||
type: 'google',
|
type: 'google',
|
||||||
accounts: [
|
accounts: [
|
||||||
{ email: 'an121245@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年1月2日 09:50 AM' }
|
{ email: '9paiii@gmail.com', lastSync: '2025-01-02 09:50 AM' }
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -27,7 +27,7 @@ export function DataSourcesList() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<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">
|
<div className="space-y-4">
|
||||||
{dataSources.map(source => (
|
{dataSources.map(source => (
|
||||||
<DataSourceItem key={source.id} {...source} />
|
<DataSourceItem key={source.id} {...source} />
|
||||||
|
|||||||
28
src/components/Auth/Sidebar.tsx
Normal file
28
src/components/Auth/Sidebar.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,7 +8,6 @@ interface UserProfileProps {
|
|||||||
export function UserProfile({ name, email }: UserProfileProps) {
|
export function UserProfile({ name, email }: UserProfileProps) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<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="flex items-center space-x-4">
|
||||||
<div className="w-12 h-12 bg-gray-100 rounded-full flex items-center justify-center">
|
<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" />
|
<User className="w-6 h-6 text-gray-500" />
|
||||||
|
|||||||
59
src/components/Auth/callback.template.ts
Normal file
59
src/components/Auth/callback.template.ts
Normal 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>
|
||||||
|
`;
|
||||||
55
src/components/Auth/login2.tsx
Normal file
55
src/components/Auth/login2.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,13 +1,133 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
import { v4 as uuidv4 } from "uuid";
|
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 { useAppStore } from "@/stores/appStore";
|
||||||
|
import { useAuthStore } from "@/stores/authStore";
|
||||||
import { OpenBrowserURL } from "@/utils/index";
|
import { OpenBrowserURL } from "@/utils/index";
|
||||||
import logoImg from "@/assets/32x32.png";
|
import logoImg from "@/assets/32x32.png";
|
||||||
|
import callbackTemplate from "@/components/Auth/callback.template";
|
||||||
|
import { clientEnv } from "@/utils/env";
|
||||||
|
|
||||||
|
|
||||||
export default function Account() {
|
export default function Account() {
|
||||||
const app_uid = useAppStore((state) => state.app_uid);
|
const app_uid = useAppStore((state) => state.app_uid);
|
||||||
const setAppUid = useAppStore((state) => state.setAppUid);
|
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() {
|
async function initializeUser() {
|
||||||
let uid = app_uid;
|
let uid = app_uid;
|
||||||
if (!uid) {
|
if (!uid) {
|
||||||
@@ -22,6 +142,15 @@ export default function Account() {
|
|||||||
// const { token } = await response.json();
|
// const { token } = await response.json();
|
||||||
// localStorage.setItem("auth_token", token);
|
// localStorage.setItem("auth_token", token);
|
||||||
OpenBrowserURL(`http://localhost:1420/login?uid=${uid}`);
|
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>) {
|
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"
|
className="text-sm/6 font-semibold text-gray-900 dark:text-gray-100"
|
||||||
onClick={LoginClick}
|
onClick={LoginClick}
|
||||||
>
|
>
|
||||||
Log In <span aria-hidden="true">→</span>
|
{loading ? "Signing In..." : "Sign In"}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import SettingsPanel from "./SettingsPanel";
|
|||||||
import GeneralSettings from "./GeneralSettings";
|
import GeneralSettings from "./GeneralSettings";
|
||||||
import AboutView from "./AboutView";
|
import AboutView from "./AboutView";
|
||||||
import Account from "./Account";
|
import Account from "./Account";
|
||||||
// import CocoCloud from "@/components/Auth/CocoCloud"
|
import CocoCloud from "@/components/Auth/CocoCloud"
|
||||||
import Footer from "../Footer";
|
import Footer from "../Footer";
|
||||||
import { useTheme } from "../../contexts/ThemeContext";
|
import { useTheme } from "../../contexts/ThemeContext";
|
||||||
import { AppTheme } from "../../utils/tauri";
|
import { AppTheme } from "../../utils/tauri";
|
||||||
@@ -25,7 +25,7 @@ function SettingsPage() {
|
|||||||
const tabs = [
|
const tabs = [
|
||||||
{ name: "General", icon: Settings },
|
{ name: "General", icon: Settings },
|
||||||
{ name: "Extensions", icon: Puzzle },
|
{ name: "Extensions", icon: Puzzle },
|
||||||
{ name: "Account", icon: User },
|
{ name: "Connect", icon: User },
|
||||||
{ name: "Advanced", icon: Settings2 },
|
{ name: "Advanced", icon: Settings2 },
|
||||||
{ name: "About", icon: Info },
|
{ name: "About", icon: Info },
|
||||||
];
|
];
|
||||||
@@ -79,7 +79,7 @@ function SettingsPage() {
|
|||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<Account />
|
<Account />
|
||||||
{/* <CocoCloud /> */}
|
<CocoCloud />
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
<TabPanel>
|
<TabPanel>
|
||||||
<SettingsPanel title="">
|
<SettingsPanel title="">
|
||||||
|
|||||||
@@ -1,55 +1,124 @@
|
|||||||
import { Github, Mail, Apple } from 'lucide-react';
|
import { useEffect } from "react";
|
||||||
|
import { GoogleOAuthProvider } from "@react-oauth/google";
|
||||||
import { useSearchParams } from "react-router-dom";
|
import { useSearchParams } from "react-router-dom";
|
||||||
|
|
||||||
import { LoginForm } from '@/components/Auth/LoginForm';
|
import { authWitheGithub } from "@/utils/index";
|
||||||
import { SocialButton } from '@/components/Auth/SocialButton';
|
import loginImg from "@/assets/images/bg-login.png";
|
||||||
import { Divider } from '@/components/Auth/Divider';
|
import logoImg from "@/assets/images/coco-logo.png";
|
||||||
import { authWitheGithub } from '@/utils/index';
|
import AppleImg from "@/assets/images/apple.png";
|
||||||
import { useEffect } from 'react';
|
import GithubImg from "@/assets/images/github.png";
|
||||||
|
import GoogleImg from "@/assets/images/google.png";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
|
const handleGoogleSignIn = (response: any) => {
|
||||||
|
console.log("Google Login Success:", response);
|
||||||
|
// response.credential
|
||||||
|
};
|
||||||
|
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const uid = searchParams.get("uid");
|
const uid = searchParams.get("uid");
|
||||||
const code = searchParams.get("code");
|
const code = searchParams.get("code");
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(() => {}, [code]);
|
||||||
|
|
||||||
}, [code])
|
|
||||||
|
|
||||||
function GithubClick() {
|
function handleGithubSignIn() {
|
||||||
uid && authWitheGithub(uid)
|
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 (
|
return (
|
||||||
<div className="min-h-screen bg-gray-900 flex items-center justify-center p-4">
|
<div className="min-h-screen bg-black flex justify-center">
|
||||||
<div className="w-full max-w-md bg-gray-800 rounded-xl p-8 shadow-2xl">
|
<div className="min-h-screen container py-[30px] relative overflow-hidden">
|
||||||
<div className="text-center mb-8">
|
{/* Background Image */}
|
||||||
<h1 className="text-2xl font-bold text-white mb-2">Welcome Back</h1>
|
<div className="absolute top-[60px] inset-0 z-0">
|
||||||
<p className="text-gray-400">Sign in to continue to Coco</p>
|
<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>
|
||||||
|
|
||||||
<div className="space-y-4 mb-6">
|
{/* Content */}
|
||||||
<SocialButton
|
<div className="relative z-10 w-full max-w-[100%] px-[130px]">
|
||||||
icon={<Github className="w-5 h-5" />}
|
{/* Logo */}
|
||||||
provider="GitHub"
|
<div className="flex items-center mb-[60px]">
|
||||||
onClick={() => GithubClick()}
|
<img src={logoImg} alt="Coco" className="h-10 w-[127px]" />
|
||||||
/>
|
</div>
|
||||||
<SocialButton
|
|
||||||
icon={<Mail className="w-5 h-5" />}
|
{/* Main Text */}
|
||||||
provider="Google"
|
<div className="text-left mb-[60px]">
|
||||||
onClick={() => console.log('Google login')}
|
<h1 className="text-[64px] leading-[72px] font-bold text-white mb-4">
|
||||||
/>
|
INSERT
|
||||||
<SocialButton
|
<br />
|
||||||
icon={<Apple className="w-5 h-5" />}
|
THE
|
||||||
provider="Apple"
|
<br />
|
||||||
onClick={() => console.log('Apple login')}
|
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>
|
</div>
|
||||||
|
|
||||||
<Divider />
|
{/* Footer */}
|
||||||
|
<div className="absolute bottom-8 w-full text-sm text-gray-500 flex justify-center">
|
||||||
<LoginForm onSubmit={(email, password) => console.log(email, password)} />
|
© {new Date().getFullYear()} Coco Labs. All rights reserved.
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
34
src/stores/authStore.ts
Normal file
34
src/stores/authStore.ts
Normal 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 }),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user