Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
178 changes: 73 additions & 105 deletions frontend/src/layouts/MainLayout.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,29 @@
<v-list-item title="Explore" :active="isExploreRoute" @click="goTo('/')" />
<v-list-item title="Trending" :active="isTrendingRoute" @click="goTo('/trending')" />
<v-list-item v-if="isAuthenticated" title="Favorites" :active="$route.path === '/favorites'" @click="goTo('/favorites')" />
<v-list-item v-if="isAuthenticated">

<v-list-group class="extensions-list-group" value="extensions">
<template #activator="{ props }">
<v-list-item
v-bind="props"
title="Extensions"
/>
</template>

<v-list-item
v-for="ext in extensions"
:key="ext.name"
:title="ext.name"
:href="ext.url"
target="_blank"
/>
</v-list-group>

<v-list-item>
<div class="d-flex align-center w-100">
<v-btn variant="text" icon="mdi-github" href="https://github.com/gorse-io/gitrec" target="_blank" />
<v-btn variant="text" icon="mdi-logout" @click="logout" />
</div>
</v-list-item>
<v-list-item v-if="!isAuthenticated">
<div class="d-flex align-center w-100">
<v-btn variant="text" icon="mdi-github" href="https://github.com/gorse-io/gitrec" target="_blank" />
<v-btn variant="text" icon="mdi-login" @click="goTo('/login')" />
<v-btn v-if="isAuthenticated" variant="text" icon="mdi-logout" @click="logout" />
<v-btn v-if="!isAuthenticated" variant="text" icon="mdi-login" @click="goTo('/login')" />
</div>
</v-list-item>
</v-list>
Expand All @@ -29,9 +42,9 @@
<span class="route-title">GitRec</span>
<v-spacer />
<div class="d-none d-md-flex align-center ga-1">
<v-btn variant="text" :to="'/'" :active="isExploreRoute" color="white">Explore</v-btn>
<v-btn variant="text" :to="'/trending'" :active="isTrendingRoute" color="white">Trending</v-btn>
<v-btn v-if="isAuthenticated" variant="text" :to="'/favorites'" :active="$route.path === '/favorites'" color="white">Favorites</v-btn>
<v-btn variant="text" to="/" :active="isExploreRoute" color="white">Explore</v-btn>
<v-btn variant="text" to="/trending" :active="isTrendingRoute" color="white">Trending</v-btn>
<v-btn v-if="isAuthenticated" variant="text" to="/favorites" :active="$route.path === '/favorites'" color="white">Favorites</v-btn>

<v-menu location="bottom end">
<template #activator="{ props }">
Expand All @@ -41,18 +54,10 @@
</template>
<v-list density="compact">
<v-list-item
title="Chrome Extension"
href="https://chrome.google.com/webstore/detail/gitrec/eihokbaeiebdenibjophfipedicippfl"
target="_blank"
/>
<v-list-item
title="Edge Add-on"
href="https://microsoftedge.microsoft.com/addons/detail/gitrec/cpcfbfpnagiffgpmfljmcdokmfjffdpa"
target="_blank"
/>
<v-list-item
title="Firefox Add-on"
href="https://addons.mozilla.org/addon/gitrec/"
v-for="ext in extensions"
:key="ext.name"
:title="ext.name"
:href="ext.url"
target="_blank"
/>
</v-list>
Expand Down Expand Up @@ -117,47 +122,54 @@
</template>

<script>
import axios from "axios";
import authMixin from "../mixins/authMixin";

const TOPICS = [
"all",
"ai",
"python",
"java",
"cpp",
"go",
"javascript",
"typescript",
"c",
"rust",
];

const LANGUAGES = [
"all",
"hackernews",
"python",
"java",
"cpp",
"go",
"javascript",
"typescript",
"c",
"rust",
];

const EXTENSIONS = [
{ name: "Chrome Extension", icon: "mdi-google-chrome", url: "https://chrome.google.com/webstore/detail/gitrec/eihokbaeiebdenibjophfipedicippfl" },
{ name: "Edge Add-on", icon: "mdi-microsoft-edge", url: "https://microsoftedge.microsoft.com/addons/detail/gitrec/cpcfbfpnagiffgpmfljmcdokmfjffdpa" },
{ name: "Firefox Add-on", icon: "mdi-firefox", url: "https://addons.mozilla.org/addon/gitrec/" },
];

export default {
mixins: [authMixin],
data() {
return {
drawer: false,
isAuthenticated: false,
topics: [
"all",
"ai",
"python",
"java",
"cpp",
"go",
"javascript",
"typescript",
"c",
"rust",
],
languages: [
"all",
"hackernews",
"python",
"java",
"cpp",
"go",
"javascript",
"typescript",
"c",
"rust",
],
topics: TOPICS,
languages: LANGUAGES,
extensions: EXTENSIONS,
};
},
computed: {
routeTitle() {
if (this.isTrendingRoute) {
return "Trending";
}
if (this.isExploreRoute) {
return "Explore";
}
if (this.isTrendingRoute) return "Trending";
if (this.isExploreRoute) return "Explore";
return this.$route.name;
},
isExploreRoute() {
Expand All @@ -172,49 +184,11 @@ export default {
activeLanguage() {
return this.$route.params.language || "all";
},
// Filter topics: hide 'ai' for anonymous users
visibleTopics() {
if (this.isAuthenticated) {
return this.topics;
}
return this.topics.filter(topic => topic !== "ai");
return this.isAuthenticated ? this.topics : this.topics.filter(t => t !== "ai");
},
},
async mounted() {
await this.checkAuth();
},
methods: {
async checkAuth() {
const cached = localStorage.getItem("gitrec_auth_state");
if (cached) {
try {
const { is_authenticated, timestamp } = JSON.parse(cached);
if (Date.now() - timestamp < 5 * 60 * 1000) {
this.isAuthenticated = is_authenticated;
return;
}
} catch (error) {
localStorage.removeItem("gitrec_auth_state");
}
}

try {
const response = await axios.get("/api/me", { withCredentials: true });
this.isAuthenticated = response.data.is_authenticated;
if (this.isAuthenticated) {
localStorage.setItem("gitrec_auth_state", JSON.stringify({
is_authenticated: true,
login: response.data.login,
timestamp: Date.now()
}));
} else {
localStorage.removeItem("gitrec_auth_state");
}
} catch (error) {
localStorage.removeItem("gitrec_auth_state");
this.isAuthenticated = false;
}
},
goTo(path) {
this.drawer = false;
if (this.$route.path !== path) {
Expand All @@ -230,21 +204,11 @@ export default {
topicLabel(topic) {
return topic.replace("-", " ").toUpperCase();
},
async logout() {
try {
await axios.get("/api/logout");
localStorage.removeItem("gitrec_auth_state");
this.isAuthenticated = false;
this.$router.push("/");
} catch (error) {
console.error("Logout failed:", error);
}
},
},
};
</script>

<style>
<style scoped>
.route-title {
font-weight: 300;
font-size: 1.2rem;
Expand All @@ -259,4 +223,8 @@ export default {
padding-top: 0;
padding-bottom: 0;
}

.extensions-list-group :deep(.v-list-group__items) {
--indent-padding: 0px;
}
</style>
79 changes: 79 additions & 0 deletions frontend/src/mixins/authMixin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import axios from 'axios';

const AUTH_CACHE_KEY = 'gitrec_auth_state';
const AUTH_CACHE_EXPIRY_MS = 5 * 60 * 1000; // 5 minutes

export default {
data() {
return {
isAuthenticated: false,
authLogin: null,
};
},
methods: {
async checkAuth() {
const cached = localStorage.getItem(AUTH_CACHE_KEY);
if (cached) {
try {
const data = JSON.parse(cached);
if (Date.now() - data.timestamp < AUTH_CACHE_EXPIRY_MS) {
this.isAuthenticated = data.is_authenticated;
this.authLogin = data.login;
return;
}
} catch {
localStorage.removeItem(AUTH_CACHE_KEY);
}
}

try {
const response = await axios.get('/api/me', { withCredentials: true });
this.isAuthenticated = response.data.is_authenticated;
this.authLogin = response.data.login;
if (this.isAuthenticated) {
localStorage.setItem(AUTH_CACHE_KEY, JSON.stringify({
is_authenticated: true,
login: response.data.login,
timestamp: Date.now()
}));
this.dispatchAuthChange(true);
} else {
localStorage.removeItem(AUTH_CACHE_KEY);
}
} catch {
this.isAuthenticated = false;
this.authLogin = null;
localStorage.removeItem(AUTH_CACHE_KEY);
}
},
async logout() {
try {
await axios.get('/api/logout', { withCredentials: true });
} catch (error) {
console.error('Logout failed:', error);
}
localStorage.removeItem(AUTH_CACHE_KEY);
this.isAuthenticated = false;
this.authLogin = null;
this.dispatchAuthChange(false);
},
dispatchAuthChange(authenticated) {
window.dispatchEvent(new CustomEvent('gitrec-auth-change', {
detail: { authenticated }
}));
},
handleAuthChange(event) {
this.isAuthenticated = event.detail.authenticated;
if (!event.detail.authenticated) {
this.authLogin = null;
}
},
},
mounted() {
this.checkAuth();
window.addEventListener('gitrec-auth-change', this.handleAuthChange);
},
beforeUnmount() {
window.removeEventListener('gitrec-auth-change', this.handleAuthChange);
},
};
Loading
Loading