feat: add login page

This commit is contained in:
Leafd 2025-10-09 13:44:52 -04:00
parent 4197762a53
commit 547117173b
No known key found for this signature in database
GPG key ID: D44AE7A3699406BE
2 changed files with 390 additions and 147 deletions

View file

@ -9,6 +9,7 @@ import Home from "./views/Home.vue";
import Projects from "./views/Projects.vue";
import Settings from "./views/Settings.vue";
import Statistics from "./views/Statistics.vue";
import Login from "./views/Login.vue";
import UserProfileCard from "./components/UserProfileCard.vue";
import CustomTitlebar from "./components/CustomTitlebar.vue";
import WakatimeSetupModal from "./components/WakatimeSetupModal.vue";
@ -411,8 +412,10 @@ async function copyApiKey() {
}
async function handleDirectOAuthAuth() {
if (!directOAuthToken.value.trim()) {
async function handleDirectOAuthAuth(token?: string) {
const tokenToUse = token || directOAuthToken.value;
if (!tokenToUse.trim()) {
alert("Please enter an OAuth authorization code or access token");
return;
}
@ -420,12 +423,12 @@ async function handleDirectOAuthAuth() {
try {
isLoading.value = true;
console.log("Attempting direct OAuth auth with token:", directOAuthToken.value);
console.log("Token length:", directOAuthToken.value.length);
console.log("Attempting direct OAuth auth with token:", tokenToUse);
console.log("Token length:", tokenToUse.length);
console.log("API config:", apiConfig.value);
await invoke("authenticate_with_direct_oauth", {
oauthToken: directOAuthToken.value,
oauthToken: tokenToUse,
apiConfig: apiConfig.value
});
@ -452,6 +455,11 @@ async function handleDirectOAuthAuth() {
}
async function checkForUpdatesAndInstall() {
if (isDevMode.value) {
console.info('[AUTO-UPDATE] Skipping auto-update check in development mode');
return;
}
try {
console.info('[AUTO-UPDATE] Checking for updates...');
const update = await check();
@ -495,158 +503,168 @@ async function checkForUpdatesAndInstall() {
<div class="flex flex-col h-screen text-text-primary font-sans outfit app-window" style="background-color: #322433;">
<CustomTitlebar />
<div class="flex flex-1 overflow-hidden">
<!-- Show login screen when not authenticated -->
<div v-if="!authState.is_authenticated" class="flex-1 overflow-hidden">
<Login
:isLoading="isLoading"
:isDevMode="isDevMode"
:oauthUrl="oauthUrl"
@authenticate="authenticate"
@handleDirectOAuthAuth="handleDirectOAuthAuth"
@openOAuthUrlManually="openOAuthUrlManually"
/>
</div>
<div v-else class="flex flex-1 overflow-hidden">
<aside class="w-64 min-w-64 flex flex-col p-0 shadow-xl relative overflow-hidden" style="background-color: #3D2C3E;">
<div class="absolute left-0 top-[76px] w-full pointer-events-none z-0">
<div class="absolute left-[63px] top-[616.5px] text-[36px] text-black opacity-20 font-light whitespace-nowrap" style="font-family: 'Outfit', sans-serif;">
01:55:58
</div>
<img src="/src/assets/suits-icons.svg" alt="" class="absolute left-[200px] top-0 w-[84px] h-[17.778px]" />
<img src="/src/assets/decorative-lines.svg" alt="" class="absolute left-0 top-[377px] w-[16px] h-[207px]" />
<img src="/src/assets/decorative-lines.svg" alt="" class="absolute left-[284px] top-[377px] w-[16px] h-[207px]" />
</div>
<div class="relative z-10 flex flex-col h-full">
<div class="p-6" style="background-color: #3D2C3E;">
<div class="flex justify-center items-center">
<img src="/src/assets/bird-illustration.svg" alt="Hackatime" class="h-12 w-auto" />
<div class="absolute left-0 top-[76px] w-full pointer-events-none z-0">
<div class="absolute left-[63px] top-[616.5px] text-[36px] text-black opacity-20 font-light whitespace-nowrap" style="font-family: 'Outfit', sans-serif;">
01:55:58
</div>
<img src="/src/assets/suits-icons.svg" alt="" class="absolute left-[200px] top-0 w-[84px] h-[17.778px]" />
<img src="/src/assets/decorative-lines.svg" alt="" class="absolute left-0 top-[377px] w-[16px] h-[207px]" />
<img src="/src/assets/decorative-lines.svg" alt="" class="absolute left-[284px] top-[377px] w-[16px] h-[207px]" />
</div>
<nav class="flex-1 py-4 px-6 space-y-5">
<button
@click="currentPage = 'home'"
class="pushable w-full"
:class="currentPage === 'home' ? 'pushable-active' : 'pushable-inactive'"
style="font-family: 'Outfit', sans-serif;"
>
<span
class="front w-full h-16 rounded-lg border-2 border-[rgba(0,0,0,0.35)] flex items-center px-4 text-xl font-bold"
:style="currentPage === 'home' ? 'background: linear-gradient(135deg, #E99682 0%, #EB9182 33%, #E88592 66%, #E883AE 100%); color: white;' : 'background-color: #543c55; color: white;'"
>
<svg class="w-8 h-8 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
</svg>
<span class="ml-auto">home</span>
</span>
</button>
<!-- Projects button -->
<button
@click="currentPage = 'projects'"
class="pushable w-full"
:class="currentPage === 'projects' ? 'pushable-active' : 'pushable-inactive'"
style="font-family: 'Outfit', sans-serif;"
>
<span
class="front w-full h-16 rounded-lg border-2 border-[rgba(0,0,0,0.35)] flex items-center px-4 text-xl font-bold"
:style="currentPage === 'projects' ? 'background: linear-gradient(135deg, #E99682 0%, #EB9182 33%, #E88592 66%, #E883AE 100%); color: white;' : 'background-color: #543c55; color: white;'"
>
<svg class="w-8 h-8 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
</svg>
<span class="ml-auto">projects</span>
</span>
</button>
<!-- Statistics button (renamed from friends in Figma, keeping your existing page) -->
<button
@click="currentPage = 'statistics'"
class="pushable w-full"
:class="currentPage === 'statistics' ? 'pushable-active' : 'pushable-inactive'"
style="font-family: 'Outfit', sans-serif;"
>
<span
class="front w-full h-16 rounded-lg border-2 border-[rgba(0,0,0,0.35)] flex items-center px-4 text-xl font-bold"
:style="currentPage === 'statistics' ? 'background: linear-gradient(135deg, #E99682 0%, #EB9182 33%, #E88592 66%, #E883AE 100%); color: white;' : 'background-color: #543c55; color: white;'"
>
<svg class="w-8 h-8 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
</svg>
<span class="ml-auto">statistics</span>
</span>
</button>
</nav>
<div class="p-6 mt-auto" style="background-color: #3D2C3E;">
<UserProfileCard
v-if="authState.is_authenticated"
:authState="authState"
:userData="userData"
:presenceData="presenceData"
:apiConfig="apiConfig"
@openSettings="currentPage = 'settings'"
/>
</div>
</div>
</aside>
<!-- Main Content Area -->
<main class="flex-1 p-6 overflow-y-auto min-w-0">
<!-- Home Page Layout -->
<div v-if="currentPage === 'home'" class="flex h-full gap-6 min-h-0 responsive-stack">
<!-- Main Home Content (Left Side - 2/3) -->
<div class="flex-1 flex flex-col min-w-0">
<Home
:authState="authState"
:apiConfig="apiConfig"
:userData="userData"
:userStats="userStats"
:weeklyChartData="weeklyChartData"
:isLoading="isLoading"
:isDevMode="isDevMode"
:oauthUrl="oauthUrl"
v-model:directOAuthToken="directOAuthToken"
@authenticate="authenticate"
@handleDirectOAuthAuth="handleDirectOAuthAuth"
@openOAuthUrlManually="openOAuthUrlManually"
/>
</div>
<!-- Leaderboard Sidebar (Right Side - 1/3) -->
<div v-if="authState.is_authenticated && userStats" class="w-64 min-w-64 flex flex-col responsive-full-width">
<div class="card-3d-app h-full">
<div class="rounded-[8px] border border-black p-4 card-3d-app-front h-full flex flex-col" style="background-color: #3D2C3E;">
<div class="flex items-center justify-between mb-4">
<h2 class="text-white text-[16px] font-bold italic m-0" style="font-family: 'Outfit', sans-serif;">
leaderboard
</h2>
<div class="flex gap-2 text-[10px]" style="font-family: 'Outfit', sans-serif;">
<span class="text-white underline cursor-pointer">friends</span>
<span class="text-white cursor-pointer">global</span>
</div>
<div class="relative z-10 flex flex-col h-full">
<div class="p-6" style="background-color: #3D2C3E;">
<div class="flex justify-center items-center">
<img src="/src/assets/bird-illustration.svg" alt="Hackatime" class="h-12 w-auto" />
</div>
<!-- Leaderboard content would go here -->
</div>
<nav class="flex-1 py-4 px-6 space-y-5">
<button
@click="currentPage = 'home'"
class="pushable w-full"
:class="currentPage === 'home' ? 'pushable-active' : 'pushable-inactive'"
style="font-family: 'Outfit', sans-serif;"
>
<span
class="front w-full h-16 rounded-lg border-2 border-[rgba(0,0,0,0.35)] flex items-center px-4 text-xl font-bold"
:style="currentPage === 'home' ? 'background: linear-gradient(135deg, #E99682 0%, #EB9182 33%, #E88592 66%, #E883AE 100%); color: white;' : 'background-color: #543c55; color: white;'"
>
<svg class="w-8 h-8 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"></path>
</svg>
<span class="ml-auto">home</span>
</span>
</button>
<!-- Projects button -->
<button
@click="currentPage = 'projects'"
class="pushable w-full"
:class="currentPage === 'projects' ? 'pushable-active' : 'pushable-inactive'"
style="font-family: 'Outfit', sans-serif;"
>
<span
class="front w-full h-16 rounded-lg border-2 border-[rgba(0,0,0,0.35)] flex items-center px-4 text-xl font-bold"
:style="currentPage === 'projects' ? 'background: linear-gradient(135deg, #E99682 0%, #EB9182 33%, #E88592 66%, #E883AE 100%); color: white;' : 'background-color: #543c55; color: white;'"
>
<svg class="w-8 h-8 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
</svg>
<span class="ml-auto">projects</span>
</span>
</button>
<!-- Statistics button (renamed from friends in Figma, keeping your existing page) -->
<button
@click="currentPage = 'statistics'"
class="pushable w-full"
:class="currentPage === 'statistics' ? 'pushable-active' : 'pushable-inactive'"
style="font-family: 'Outfit', sans-serif;"
>
<span
class="front w-full h-16 rounded-lg border-2 border-[rgba(0,0,0,0.35)] flex items-center px-4 text-xl font-bold"
:style="currentPage === 'statistics' ? 'background: linear-gradient(135deg, #E99682 0%, #EB9182 33%, #E88592 66%, #E883AE 100%); color: white;' : 'background-color: #543c55; color: white;'"
>
<svg class="w-8 h-8 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
</svg>
<span class="ml-auto">statistics</span>
</span>
</button>
</nav>
<div class="p-6 mt-auto" style="background-color: #3D2C3E;">
<UserProfileCard
:authState="authState"
:userData="userData"
:presenceData="presenceData"
:apiConfig="apiConfig"
@openSettings="currentPage = 'settings'"
/>
</div>
</div>
</div>
<!-- Statistics Page Layout (full page) -->
<div v-else-if="currentPage === 'statistics'" class="flex flex-col h-full">
<Statistics :apiConfig="apiConfig" />
</div>
</aside>
<!-- Settings Page Layout (no outer card) -->
<div v-else-if="currentPage === 'settings'" class="flex flex-col h-full">
<Settings
:apiKey="apiKey"
v-model:showApiKey="showApiKey"
@copyApiKey="copyApiKey"
@logout="logout"
@checkWakatimeConfig="openWakatimeConfigModal"
/>
</div>
<!-- Main Content Area -->
<main class="flex-1 p-6 overflow-y-auto min-w-0">
<!-- Home Page Layout -->
<div v-if="currentPage === 'home'" class="flex h-full gap-6 min-h-0 responsive-stack">
<!-- Main Home Content (Left Side - 2/3) -->
<div class="flex-1 flex flex-col min-w-0">
<Home
:authState="authState"
:apiConfig="apiConfig"
:userData="userData"
:userStats="userStats"
:weeklyChartData="weeklyChartData"
:isLoading="isLoading"
:isDevMode="isDevMode"
:oauthUrl="oauthUrl"
v-model:directOAuthToken="directOAuthToken"
@authenticate="authenticate"
@handleDirectOAuthAuth="handleDirectOAuthAuth"
@openOAuthUrlManually="openOAuthUrlManually"
/>
</div>
<!-- Projects Page Layout -->
<div v-else class="flex flex-col h-full">
<Projects :apiConfig="apiConfig" />
</div>
</main>
<!-- Leaderboard Sidebar (Right Side - 1/3) -->
<div v-if="authState.is_authenticated && userStats" class="w-64 min-w-64 flex flex-col responsive-full-width">
<div class="card-3d-app h-full">
<div class="rounded-[8px] border border-black p-4 card-3d-app-front h-full flex flex-col" style="background-color: #3D2C3E;">
<div class="flex items-center justify-between mb-4">
<h2 class="text-white text-[16px] font-bold italic m-0" style="font-family: 'Outfit', sans-serif;">
leaderboard
</h2>
<div class="flex gap-2 text-[10px]" style="font-family: 'Outfit', sans-serif;">
<span class="text-white underline cursor-pointer">friends</span>
<span class="text-white cursor-pointer">global</span>
</div>
</div>
<!-- Leaderboard content would go here -->
</div>
</div>
</div>
</div>
<!-- Statistics Page Layout (full page) -->
<div v-else-if="currentPage === 'statistics'" class="flex flex-col h-full">
<Statistics :apiConfig="apiConfig" />
</div>
<!-- Settings Page Layout (no outer card) -->
<div v-else-if="currentPage === 'settings'" class="flex flex-col h-full">
<Settings
:apiKey="apiKey"
v-model:showApiKey="showApiKey"
@copyApiKey="copyApiKey"
@logout="logout"
@checkWakatimeConfig="openWakatimeConfigModal"
/>
</div>
<!-- Projects Page Layout -->
<div v-else class="flex flex-col h-full">
<Projects :apiConfig="apiConfig" />
</div>
</main>
</div>
<!-- Configuration Modal -->

225
src/views/Login.vue Normal file
View file

@ -0,0 +1,225 @@
<template>
<div class="flex items-center justify-center h-full w-full" style="background-color: #322433;">
<div class="max-w-md w-full px-8">
<div v-if="!authInProgress" class="flex justify-center mb-8">
<img src="/src/assets/bird-illustration.svg" alt="Hackatime" class="h-24 w-auto" />
</div>
<div class="card-3d">
<div class="rounded-[12px] border-2 border-black card-3d-front p-8" style="background-color: #3D2C3E;">
<div class="text-center">
<h1 v-if="!authInProgress" class="text-[32px] font-bold text-white mb-8" style="font-family: 'Outfit', sans-serif;">
Welcome to Hackatime
</h1>
<div v-if="!authInProgress">
<button
@click="handleLogin"
:disabled="isLoading"
class="pushable w-full mt-8"
:class="isLoading ? '' : 'pushable-active'"
style="font-family: 'Outfit', sans-serif;"
>
<span
class="front w-full h-16 px-8 rounded-lg border-2 border-[rgba(0,0,0,0.35)] font-bold text-xl flex items-center justify-center gap-3"
:style="isLoading ? 'background-color: #543c55; color: white;' : 'background: linear-gradient(135deg, #E99682 0%, #EB9182 33%, #E88592 66%, #E883AE 100%); color: white;'"
>
<svg v-if="!isLoading" class="w-7 h-7 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2.5">
<path stroke-linecap="round" stroke-linejoin="round" d="M11 16l-4-4m0 0l4-4m-4 4h14m-5 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h7a3 3 0 013 3v1"></path>
</svg>
<svg v-else class="animate-spin h-7 w-7 flex-shrink-0" fill="none" viewBox="0 0 24 24">
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>
<span class="flex-shrink-0">{{ isLoading ? 'Initializing...' : 'Sign in with Hackatime' }}</span>
</span>
</button>
<div v-if="isDevMode" class="mt-8 pt-8 border-t border-white/20">
<p class="text-white/60 text-sm mb-3" style="font-family: 'Outfit', sans-serif;">
Developer Mode: Paste token directly
</p>
<div class="flex gap-2">
<input
v-model="directToken"
type="text"
placeholder="Paste token..."
class="flex-1 p-3 bg-[#2A1F2B] border border-white/20 rounded-lg text-white font-mono text-sm focus:outline-none focus:border-[#E99682] transition-colors"
@keyup.enter="handleDirectAuth"
/>
<button
@click="handleDirectAuth"
:disabled="!directToken.trim() || isLoading"
class="px-4 py-3 rounded-lg border-2 border-[rgba(0,0,0,0.35)] font-bold text-sm transition-all"
:class="!directToken.trim() || isLoading ? 'bg-gray-600 text-white/50 cursor-not-allowed' : 'bg-[#E99682] text-white hover:bg-[#d88672]'"
style="font-family: 'Outfit', sans-serif;"
>
Go
</button>
</div>
</div>
</div>
<div v-else class="text-center py-4">
<div class="mb-8 loader-container">
<RandomLoader />
</div>
<h2 class="text-[28px] font-bold text-white mb-5" style="font-family: 'Outfit', sans-serif;">
Opening in your browser
</h2>
<p class="text-white/70 text-[16px]" style="font-family: 'Outfit', sans-serif;">
Complete authentication in the browser window that just opened
</p>
<button
@click="handleManualOpen"
class="pushable w-full mt-10 mb-6"
style="font-family: 'Outfit', sans-serif; background-color: #2A1F2B;"
>
<span
class="front w-full h-14 px-6 rounded-lg border-2 border-[rgba(0,0,0,0.35)] font-medium text-base flex items-center justify-center"
style="background-color: #543c55; color: white;"
>
Didn't work? Click here to open manually
</span>
</button>
<button
@click="cancelAuth"
class="text-white/60 text-base hover:text-white transition-colors font-medium"
style="font-family: 'Outfit', sans-serif;"
>
Cancel
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import RandomLoader from '../components/RandomLoader.vue';
const emit = defineEmits<{
authenticate: [];
handleDirectOAuthAuth: [token: string];
openOAuthUrlManually: [];
}>();
defineProps<{
isLoading: boolean;
isDevMode: boolean;
oauthUrl: string | null;
}>();
const authInProgress = ref(false);
const directToken = ref('');
async function handleLogin() {
emit('authenticate');
setTimeout(() => {
authInProgress.value = true;
}, 500);
}
function handleManualOpen() {
emit('openOAuthUrlManually');
}
function cancelAuth() {
authInProgress.value = false;
}
function handleDirectAuth() {
if (directToken.value.trim()) {
emit('handleDirectOAuthAuth', directToken.value.trim());
directToken.value = '';
}
}
</script>
<style scoped>
.card-3d {
position: relative;
border-radius: 12px;
padding: 0;
}
.card-3d::before {
content: '';
position: absolute;
inset: 0;
border-radius: 12px;
background-color: #2A1F2B;
z-index: 0;
}
.card-3d-front {
position: relative;
transform: translateY(-8px);
z-index: 1;
}
.pushable {
border-radius: 12px;
border: none;
padding: 0;
cursor: pointer;
outline-offset: 4px;
position: relative;
}
.pushable-active {
background: linear-gradient(135deg, #B85E6D 0%, #B85E6D 33%, #B5546F 66%, #B55389 100%);
}
.pushable:not(.pushable-active) {
background-color: #2A1F2B;
}
.pushable:disabled {
cursor: not-allowed;
opacity: 0.6;
}
.front {
display: flex;
align-items: center;
border-radius: 12px;
transform: translateY(-6px);
transition: transform 0.1s ease;
position: relative;
}
.pushable:active:not(:disabled) .front {
transform: translateY(-2px);
}
/* Bounce Animation */
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-10px);
}
}
.animate-bounce {
animation: bounce 1s infinite;
}
.loader-container {
height: 120px;
display: flex;
align-items: center;
justify-content: center;
}
.loader-container :deep(.flex) {
height: 120px !important;
}
</style>