From 7d7952ff81ec8fb25c9b22eccd08de008efa599f Mon Sep 17 00:00:00 2001
From: paidge <paidge_cs@hotmail.com>
Date: Mon, 24 Jan 2022 14:01:08 +0100
Subject: [PATCH] add favourites page

---
 assets/img/favori_add.png    | Bin 0 -> 3137 bytes
 assets/img/favori_remove.png | Bin 0 -> 2958 bytes
 components/btn/Clipboard.vue |   2 +-
 components/member/Card.vue   |  71 +++++-
 graphql/cache.js             |  41 ++--
 graphql/queries.js           | 425 ++++++++++++++++-------------------
 i18n/locales/en.json         |   6 +
 i18n/locales/es.json         |   6 +
 i18n/locales/fr.json         |   6 +
 layouts/default.vue          |   5 +-
 pages/favoris.vue            |  94 ++++++++
 11 files changed, 408 insertions(+), 248 deletions(-)
 create mode 100644 assets/img/favori_add.png
 create mode 100644 assets/img/favori_remove.png
 create mode 100644 pages/favoris.vue

diff --git a/assets/img/favori_add.png b/assets/img/favori_add.png
new file mode 100644
index 0000000000000000000000000000000000000000..f02f205a73bf82eaa22a74f72deaf0d00568e220
GIT binary patch
literal 3137
zcmV-H48HS;P)<h;3K|Lk000e1NJLTq003kF003kN1^@s6aN?Cz00001b5ch_0Itp)
z=>Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D3)V?QK~#8N?VStk
z97P?+clP>#7D``01yqng@Bz{oh(-|*5X1)t1r>@SQ3;}Af+_J4gAXJ=0nsQyG!lyn
zQh7rJB3cST-a(Nd4-04uwME<77wxUz@6O!cy}8+)ncdsH+2%gUcW>@)W^Nw4zukFW
zG-k}00)%>PE0d!rK3PP`JLpa#u{4Uhm%_a!{dP>FOcznS1WGhLh3{T2y^LuL5sC8+
zm7yeVQQpQh#8edgs=<nw&`a>MyY$*FV)fcK-U!W>P9u|$wG)-QRkX4~V3OXioVnm>
z<$cOIre-F?B>ht}Q6_DM-<@TU_Rz*F<PFfvq_Yq{5}%;lt)X=l;$p6Ppnc-(cT71t
zFh$EW(^RBQ2s29tZ3k_;LS7HOLOPw8f-JV`=H99bfw}U&C%|mA?mp5MVwj-EHIw9`
zjR-qKMiD-3x<XzJ?IWFDOhq=2Q|{ntQH8{%pP0r^TQvaF^RQ-$e6$`>OqVf*O<S%I
znyLe&vlQDStM5|oplL~kIGL+e>ea~DYz+~ZoclBrr0W4Z5?^ZgJgc>cYMP8OT-tDj
z><O{|Di;w&i<2n+t$Lfs4ru5frQBo66Q6B3+xrAh`$8PF`nUdFzr><6$}#0ZLEbf3
zuzh+&d4*qbx#9b)R>K?GCE*gf$>K09M+XZ?CQKE~)?($%V;0QTp~^kRpZH9}S>7jj
zdad-E!kH-RbU*zPmnpwvd_m#eVJ_4OJ(haQfG}4Y{AINQ-X=@m;SsXQnup>&M7eWF
zuH`|xB}pW%R?a+jh2AWkV?2mYH=Nylf~Wb?Zy!71yN$Zveu+zz-!ZPB(0*qw%n3c2
zdNY8q*Bbn1^&Gr)r0=i@)nv^@u^z76IV9KdAl?1IZ2d+#^Wf9lLDCr(fmTTTP8=pl
zx0xhLex;lSx;Ns$*^9a0D*BLej<JEly3Sx`vU&#I3QZ2;xWy1ID^AJ|?t~2S3Ym%G
zJX*PPNN&Jkt8|-jFk5>;oHP*>0ai$S3=UJI+f1>!>e0O(hr&t=(QIX(QrH{H8{J4p
z)<o3(430&;vd<?`cMrU7k#F;XS^7O*4wLVUJcTRiEae^*n{nPefY*P?w<+;SNW7(|
zG!*%Ceb~ER0?{U<^~+2!uaKkg0T;N0`Uu<yu8z0k<yQI5%I?V9vv@VOS3(2N@Cyab
zI9%4E=&}xpRj82N@XDF(P9T4G;@Fn1lGV5>#KG!K3rKFl!MQSs!_!ts%%+**piL`C
zqWDVX^q^}S+6@X+$o~g;0%rxDMs8DXAC^IPNN4aY<;`vuZhU{l>+8~w!ek>%&+({`
zo7H<?_EmXg*L$d}#QpKm-qXhn-25)V{p161`#zCufR;nXj=qZEaWHuV4Z-u_epr4q
zz%L<=8gk%?ZHm7_Ch){1zNl{Sb+xtTKr^M&hh!=8crqNknkPU-#2>|(v=%ZwE9a?;
zjD8>Cx0BH{3fB<wMCj=xXcWNo$Dcx3Y|_fcrp+k3uOWP4zZ@^``?Y{BdI(|nlTkE=
zULhZbnr3Gd>J5hSu!-#>=Lr{6@FN!@&{i!HoBqqI4B?w<i0Kt_8q^@2Bar9iS{XP<
zT!X-6mfCfWK?ExXBeY4d^KT~^N3G};@^PpMc*1@IdG$SEalehvMEFW4?5_CnTMd!h
z^fba+=2TT?6cw@;@^-hDR~J2su=~p>s>7SsIf(Qn?MG}vt2)M$O>O;T4Od8fBJxzC
z6E>YxvAv9AxEzchcw+Det-2jy>(~sYsS4Qvg=(8!Z_+B5{n{6uvdyx{@Z&bEMB?79
z;usc9RLFd!{V^>sE?R=H-urfyy}7W>ataO?!|k)e6@`}k0F9t?;AVr;2A3lI1ma9X
ziW47tZi>(EdqX`Qhd8-`9gx}AE<_lY!)Rj);*LL!Jbn}I!Vyf3Ey&|}$m7?gyIJ<e
znQ*UmX1Iv0Zmz1e!FEOV3aF}|K`!h9D{!MB-8OOZ#am?*qhm4>{~aV|i<UT>*k<_{
z!WUho?8-3}Y+3ynve4mxU75B8XWkd>;pIeGlVzZDw1|?Q!{OP1mt2j@o3{T&`MwXD
zC|>$mXr^ecXhzs?l%1$>7euo~IKhD?gC?sdcHiF*nPA)ED<~40Q=f8KkDC4*tRgF$
zl^zuHkSW`ID3jQ!R%TR>+cagbfK#C|yb5W!M@y57{*168_mZcu&9V@Q-K-_vCY;f+
zZ`X%jAqyZ+CLMy`UZbVOCU&UuiU@i&md-&)_}{dQ*u<wIpY1dB3i%-91KJnfT|=v3
zHIG-}hfZBBXmW27b?2j;xX_Y?<pS;v;uPg>fX)#(_}ufBAWsM4_GDW+SDHF2gsj62
zdjV>Lh0Jy0R&Y0+r<@ItxNH?jwjdAZqB1xmpO3b+QFnzXx{Ku165S`?=aWQ&lb{>I
z>9?Li+TVse;0dgFYU(g(s6tYn#94NQa_4~-b=S5eZHR1Lg2OuL&LiPiJpDC7lNBOh
zj@ZyEf{QqxkWMG6p0OKH;2bO}Kpcxh;aZipvIm|}3S0BY{8yD<ANZ*JE$Hm@oj0B1
zOlipqepo5n8u2GhnazkDfI}O=@qlaF*K29fB-kmrZGlYL)|}@q;;b8_?J7fhvRVmm
zjhOeQ?jgG%+m^18gmZ9V9-0SFc2_B99&=TjrFI&skoY|~*cNeCnH0?{tHRaQ<%~#f
zx}1xMy0x{=8wV!gW`h~Xik*vP3X~HtE=Krgq2+LM7UOH=SAHPuHJS97#F>#zK9$zS
zOk~E=(I~#B{Cbd1QF?wbu5_x+>G0rL!SMn^Tz$etKJvk*A;6(ta|U~Yr?*JIPE17w
zeiz}odOZCSoYn6$V8j#0G8I%@sQk7UWWu)11t)5|p3VEfzit1;thj}S^I46wx|r5h
zo2?nhgzc>Rq4H}XH7S<8u#7@$UYv^v3MVv6;?oS_vtnB*$SpY>Nhg+>GBzQU(~-(^
z9y_AaY+Ls^NU%T3_Rk7<NU%)?6r6?9PLTMj4X!eSTt%`DE+Nf-FoesBeQgyUu}23-
z<2GoS(UjXI!8uW!y6U^$!bQE|*PK0k<1-9lvKmzvPgOeaHDsnw@WX91baq35+4jmB
zhW>)zG8HV>w%a;ExyYtnpiqBd%1lPw8eD6=caFv9FKGT8Bz_H@2e<Fsu-@{3KC_9>
zZL`~1nXNKg(~xM}?x2?|zhpbScW7G-Xf4W;-S4V!I)~%4=QKYK65P9Su8f1kXPNy?
zh~qHaytyr;XSyjP6M~=KpRBNLQDzC<g9vLXlsPEi>ka;GLYt-2+r^G+KnfS*pwhmi
z{Duz-<fo=gWb_|+;~#dN3b$|D0tF>5LU_(z%mY_U{8{+5JX8#bZ59ra@y{zC)V|Zj
z6QUKs{8;y3zX`g+{9U2Aq`~KZKn_HhGD``TB7N+%G=0|-r>Sywe(0nNAx#;mC{cFN
z&vb;n3~~70pRB@FxKRB@xFGXCG9|u(xR=xy5Ou{qq_o%IMJ?{Cx<^GGEK`ukm0F^!
zh{L+WTsXQX;)FRe{#F^$Y;n7&VBy4NQ^I}4)-(s3c9hW$l}U)3OPYNbuo{IcZ`4}^
z=7Wr0WJ<KB)*zD~QBl@N=P>+Mi8jlC8k<Dt3(mawA6jC4#l<vb{yefQvvCsH_6W9#
z3xT-!G&s0q+LT}y(L`N<IEva9b-^|ZH_P;GvrGu#&rorN0}Axpro=kv1(=TzW(Mw$
zv^=V%%~9zic~J0=gP$Hus7+}`8eD#0Wg5~{W`h7OG^)JdbC?`pNMw=Nw&3E*K*y)k
zHp^-)&4of3?t&Bw@h41)%n?W2+OQMqE0NYawX_!~9rUWqu+C6+7TUmBl=K$v{7HTk
zG|S|QDd!y-@VFihb8wav_1dDs(zZn_$D%nw4te10NK@V@4(q;2`VEG33XN$Dfy>yM
zY+KX?+boL>`GbO;K-W1EiQ{Hxe}^7IC4C+rj-qW*7i_a|CMN%VlzF_n-B<p$#kdl-
beWK`pGv=)CqDozc00000NkvXXu0mjfG=L7=

literal 0
HcmV?d00001

diff --git a/assets/img/favori_remove.png b/assets/img/favori_remove.png
new file mode 100644
index 0000000000000000000000000000000000000000..06b43589048636ea9ff8bab2dd43b0f598a8c2e2
GIT binary patch
literal 2958
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
Ef?2<fcmMzZ

literal 0
HcmV?d00001

diff --git a/components/btn/Clipboard.vue b/components/btn/Clipboard.vue
index eb8c5f0..121b4f5 100644
--- a/components/btn/Clipboard.vue
+++ b/components/btn/Clipboard.vue
@@ -33,7 +33,7 @@ export default {
 			$("#btncopy").tooltip("show")
 			setTimeout(() => {
 				$("#btncopy").tooltip("hide")
-			}, 500)
+			}, 1000)
 		}
 	}
 }
diff --git a/components/member/Card.vue b/components/member/Card.vue
index 442ce2f..10bae06 100644
--- a/components/member/Card.vue
+++ b/components/member/Card.vue
@@ -1,7 +1,15 @@
 <template>
 	<div class="card member">
+		<button
+			id="favori"
+			class="btn btn-light position-absolute"
+			:class="{
+				add: !isFavorite,
+				remove: isFavorite
+			}"
+			@click="toggleFavourite"></button>
 		<div class="card-body">
-			<h2 class="card-title text-center">
+			<h2 class="card-title text-center mb-4">
 				{{ hash.uid }}
 				<BadgeStatus :membre="hash" />
 			</h2>
@@ -121,13 +129,74 @@
 
 <script>
 export default {
+	data() {
+		return {
+			favourites: []
+		}
+	},
 	props: {
 		hash: Object
+	},
+	methods: {
+		toggleFavourite() {
+			let $this = this
+
+			$("#favori").tooltip({
+				title: function () {
+					return $this.isFavorite
+						? $this.$t("favoris.supprime")
+						: $this.$t("favoris.enregistre")
+				},
+				html: true,
+				trigger: "manual"
+			})
+
+			$("#favori").tooltip("show")
+			setTimeout(() => {
+				$("#favori").tooltip("hide")
+			}, 1000)
+
+			if (!this.isFavorite) {
+				this.favourites.push(this.hash.uid)
+			} else {
+				this.favourites.splice(this.favourites.indexOf(this.hash.uid), 1)
+			}
+
+			localStorage.favourites = JSON.stringify(this.favourites)
+		}
+	},
+	computed: {
+		isFavorite() {
+			this.favourites = localStorage.favourites
+				? JSON.parse(localStorage.favourites)
+				: []
+
+			return this.favourites.includes(this.hash.uid)
+		}
 	}
 }
 </script>
 
 <style lang="scss">
+#favori {
+	top: 1.25rem;
+	left: 1.25rem;
+	background-color: var(--light);
+	background-size: 75%;
+	background-repeat: no-repeat;
+	background-position: center;
+	width: 50px;
+	height: 50px;
+
+	&.add {
+		background-image: url("~/assets/img/favori_add.png");
+	}
+
+	&.remove {
+		background-image: url("~/assets/img/favori_remove.png");
+	}
+}
+
 .member {
 	.table {
 		text-align: center;
diff --git a/graphql/cache.js b/graphql/cache.js
index 21055e0..b8dff3a 100644
--- a/graphql/cache.js
+++ b/graphql/cache.js
@@ -1,21 +1,32 @@
-import { InMemoryCache, IntrospectionFragmentMatcher, defaultDataIdFromObject } from 'apollo-cache-inmemory'
-import introspectionQueryResultData from './fragmentTypes.json';
+import {
+	InMemoryCache,
+	IntrospectionFragmentMatcher,
+	defaultDataIdFromObject
+} from "apollo-cache-inmemory"
+import introspectionQueryResultData from "./fragmentTypes.json"
 
 const fragmentMatcher = new IntrospectionFragmentMatcher({
-    introspectionQueryResultData
+	introspectionQueryResultData
 })
 
 // Apparemment il faut utiliser la syntaxe Apollo v2
 export const cache = new InMemoryCache({
-    addTypename: false,
-    fragmentMatcher,
-    dataIdFromObject: object => {
-        switch (object.__typename) {
-            case 'Identity': return object.hash
-            case 'Event': return object.block.number
-            case 'EventId': return `${object.member.hash}:${object.inOut}`
-            case 'Forecast': return `${object.member.hash}:${object.date}:${object.after}:${object.proba}`
-            default: return defaultDataIdFromObject(object); // fall back to default handling
-        }
-    }
-})
\ No newline at end of file
+	addTypename: false,
+	fragmentMatcher,
+	dataIdFromObject: (object) => {
+		switch (object.__typename) {
+			case "Identity":
+				return object.hash
+			case "Event":
+				return object.block.number
+			case "EventId":
+				return `${object.member.hash}:${object.inOut}`
+			case "Forecast":
+				return `${object.member.hash}:${object.date}:${object.after}:${object.proba}`
+			case "GroupId":
+				return `${object.id.hash}`
+			default:
+				return defaultDataIdFromObject(object) // fall back to default handling
+		}
+	}
+})
diff --git a/graphql/queries.js b/graphql/queries.js
index 0df3c0f..5e0b8dd 100644
--- a/graphql/queries.js
+++ b/graphql/queries.js
@@ -1,243 +1,208 @@
 import gql from "graphql-tag"
 
 // Pour la sidebar
-export const LAST_BLOCK = gql`query LastBlock{
-	countMax {
-    number
-    bct
-    utc0
-  }
-}`
+export const LAST_BLOCK = gql`
+	query LastBlock {
+		countMax {
+			number
+			bct
+			utc0
+		}
+	}
+`
 
 // Pour la page index
-export const LAST_EVENTS = gql`query LastEvents($start: Int64, $end: Int64) {
-  membersCount(start: $start, end: $end) {
-    idList {
-      __typename
-      member : id {
-        __typename
-        pubkey
-        uid
-        status
-        hash
-        limitDate
-        history {
-          __typename
-          in
-          block {
-            __typename
-            number
-          }
-        }
-        received_certifications {
-          __typename
-          limit
-        }
-      }
-      inOut
-    },
-    block {
-      __typename
-      number
-    }
-  }
-} `
-
-// Pour la page previsions/index
-export const PREVISIONS = gql`query GetDossiers {
-    now {
-      number
-      bct
-      __typename
-    }
-    parameter(name: sigQty) {
-      sigQty: value
-      __typename
-    }
-    wwFile(full: true) {
-      certifs_dossiers {
-        ... on MarkedDatedCertification {
-          datedCertification {
-            date
-            certification {
-              from {
-                uid
-                __typename
-              }
-              to {
-                uid
-                __typename
-              }
-              expires_on
-              __typename
-            }
-            __typename
-          }
-          __typename
-        }
-        ... on MarkedDossier {
-          dossier {
-            main_certifs
-            newcomer {
-              uid
-              lastApplication {
-                lastAppDate: bct
-                __typename
-              }
-              distance: distanceE {
-                value {
-                  ratio
-                  __typename
-                }
-                dist_ok
-                __typename
-              }
-              __typename
-            }
-            date
-            minDate
-            expires_on: limit
-            certifications {
-              date
-              certification {
-                from {
-                  uid
-                  quality {
-                    ratio
-                    __typename
-                  }
-                  __typename
-                }
-                expires_on
-                __typename
-              }
-              __typename
-            }
-            __typename
-          }
-          __typename
-        }
-      }
-      __typename
-    }
-  }`
+export const LAST_EVENTS = gql`
+	query LastEvents($start: Int64, $end: Int64) {
+		membersCount(start: $start, end: $end) {
+			idList {
+				__typename
+				member: id {
+					__typename
+					pubkey
+					uid
+					status
+					hash
+					limitDate
+					history {
+						__typename
+						in
+						block {
+							__typename
+							number
+						}
+					}
+					received_certifications {
+						__typename
+						limit
+					}
+				}
+				inOut
+			}
+			block {
+				__typename
+				number
+			}
+		}
+	}
+`
 
 // Pour la page previsions/newcomers
-export const NEWCOMERS = gql`query GetNewcomers{
-    wwResult {
-        __typename
-        permutations_nb
-        dossiers_nb
-        certifs_nb
-        forecastsByNames {
-            __typename
-            member : id {
-                __typename
-                pubkey
-                uid
-                status
-                hash
-                limitDate
-                received_certifications {
-                    __typename
-                    limit
-                }
-            }
-            date
-            after
-            proba
-        }
-    }
-} `
+export const NEWCOMERS = gql`
+	query GetNewcomers {
+		wwResult {
+			__typename
+			permutations_nb
+			dossiers_nb
+			certifs_nb
+			forecastsByNames {
+				__typename
+				member: id {
+					__typename
+					pubkey
+					uid
+					status
+					hash
+					limitDate
+					received_certifications {
+						__typename
+						limit
+					}
+				}
+				date
+				after
+				proba
+			}
+		}
+	}
+`
 
 // Pour la page membres/index
-export const SEARCH_MEMBERS = gql`query SearchMember($hint: String) {
-    idSearch(with: {hint: $hint}) {
-        __typename
-        ids {
-            __typename
-            pubkey
-            uid
-            status
-            hash
-            limitDate
-            received_certifications {
-                __typename
-                limit
-            }
-        }
-    }
-} `
+export const SEARCH_MEMBERS = gql`
+	query SearchMember($hint: String) {
+		idSearch(with: { hint: $hint }) {
+			__typename
+			ids {
+				__typename
+				pubkey
+				uid
+				status
+				hash
+				limitDate
+				received_certifications {
+					__typename
+					limit
+				}
+			}
+		}
+	}
+`
 
 // Pour la page membres/_hash
-export const SEARCH_MEMBER = gql`query SearchMemberWithHash($hash: Hash!) {
-    idFromHash(hash: $hash) {
-        ...attr
-        pubkey
-        isLeaving
-        sentry
-        membership_pending
-        limitDate
-        distanceE {
-          __typename
-          value {
-            __typename
-            ratio
-          }
-          dist_ok
-        }
-        distance {
-          __typename
-          value {
-            __typename
-            ratio
-          }
-          dist_ok
-        }
-        received_certifications {
-          __typename
-          certifications {
-            __typename
-            from {
-              ...attr
-            }
-            expires_on
-            pending
-          }
-        }
-        sent_certifications {
-          __typename
-          to {
-            ...attr
-          }
-          expires_on
-          pending
-        }
-    }
-}
-fragment attr on Identity {
-  __typename
-  uid
-  hash
-  status
-  minDate
-  minDatePassed
-  quality {
-    __typename
-    ratio
-  }
-  received_certifications {
-    __typename
-    limit
-  }
-}`
+export const SEARCH_MEMBER = gql`
+	query SearchMemberWithHash($hash: Hash!) {
+		idFromHash(hash: $hash) {
+			...attr
+			pubkey
+			isLeaving
+			sentry
+			membership_pending
+			limitDate
+			distanceE {
+				__typename
+				value {
+					__typename
+					ratio
+				}
+				dist_ok
+			}
+			distance {
+				__typename
+				value {
+					__typename
+					ratio
+				}
+				dist_ok
+			}
+			received_certifications {
+				__typename
+				certifications {
+					__typename
+					from {
+						...attr
+					}
+					expires_on
+					pending
+				}
+			}
+			sent_certifications {
+				__typename
+				to {
+					...attr
+				}
+				expires_on
+				pending
+			}
+		}
+	}
+	fragment attr on Identity {
+		__typename
+		uid
+		hash
+		status
+		minDate
+		minDatePassed
+		quality {
+			__typename
+			ratio
+		}
+		received_certifications {
+			__typename
+			limit
+		}
+	}
+`
 
 // Pour la page parametres
-export const PARAMS = gql`query getParams{
-  allParameters {
-    name
-    par_type
-    value
-    comment
-  }
-}`
\ No newline at end of file
+export const PARAMS = gql`
+	query getParams {
+		allParameters {
+			name
+			par_type
+			value
+			comment
+		}
+	}
+`
+// Pour la page favoris
+export const FAVORIS = gql`
+	query getFavoris($group: [String!]!) {
+		filterGroup(group: $group) {
+			__typename
+			selected {
+				__typename
+				id {
+					...attr
+				}
+			}
+			others {
+				__typename
+				id {
+					...attr
+				}
+			}
+		}
+	}
+	fragment attr on Identity {
+		__typename
+		pubkey
+		uid
+		status
+		hash
+		limitDate
+		received_certifications {
+			__typename
+			limit
+		}
+	}
+`
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
index 3346c2a..fbfd6b4 100644
--- a/i18n/locales/en.json
+++ b/i18n/locales/en.json
@@ -62,6 +62,12 @@
 		"title": "Duniter"
 	},
 	"expire": "Expires",
+	"favoris": {
+		"enregistre": "Saved to favorites&nbsp;!",
+		"none": "You don't have any favorites yet",
+		"supprime": "Deleted from favourites&nbsp;!",
+		"title": "My favourites"
+	},
 	"futuremembers": "Future members",
 	"infos": "Informations",
 	"inout": "Entries and exits of the web of trust for the last 2 days",
diff --git a/i18n/locales/es.json b/i18n/locales/es.json
index 431d87e..22e9c86 100644
--- a/i18n/locales/es.json
+++ b/i18n/locales/es.json
@@ -62,6 +62,12 @@
 		"title": "Duniter"
 	},
 	"expire": "Expira el",
+	"favoris": {
+		"enregistre": "¡Guardado en favoritos!",
+		"none": "Aún no tienes favoritos",
+		"supprime": "¡Eliminado de favoritos!",
+		"title": "Mis favoritos"
+	},
 	"futuremembers": "Futuros miembros",
 	"infos": "Informaciones",
 	"inout": "Entradas y salidas de la red de confianza en los últimos 2 días",
diff --git a/i18n/locales/fr.json b/i18n/locales/fr.json
index c0a1b10..0407b24 100644
--- a/i18n/locales/fr.json
+++ b/i18n/locales/fr.json
@@ -62,6 +62,12 @@
 		"title": "Duniter"
 	},
 	"expire": "Expire le",
+	"favoris": {
+		"enregistre": "Enregistré dans les favoris&nbsp;!",
+		"none": "Vous n'avez pas encore de favoris",
+		"supprime": "Supprimé des favoris&nbsp;!",
+		"title": "Mes favoris"
+	},
 	"futuremembers": "Futurs membres",
 	"infos": "Informations",
 	"inout": "Entrées et sorties de la toile de confiance des 2 derniers jours",
diff --git a/layouts/default.vue b/layouts/default.vue
index f3ee63e..651572f 100644
--- a/layouts/default.vue
+++ b/layouts/default.vue
@@ -14,7 +14,10 @@ export default {
 			menus: [
 				{
 					title: "wot.title",
-					items: [{ path: "/membres", title: "membres" }]
+					items: [
+						{ path: "/membres", title: "membres" },
+						{ path: "/favoris", title: "favoris.title" }
+					]
 				},
 				{
 					title: "previsions.title",
diff --git a/pages/favoris.vue b/pages/favoris.vue
new file mode 100644
index 0000000..a17eb94
--- /dev/null
+++ b/pages/favoris.vue
@@ -0,0 +1,94 @@
+<template>
+	<main class="container">
+		<h2 class="text-center my-5 font-weight-light">
+			{{ $t("favoris.title") }}
+		</h2>
+
+		<NavigationLoader :isLoading="$apollo.queries.favoris.loading" />
+		<div class="row text-center">
+			<div class="col">
+				<transition name="fade">
+					<div class="alert alert-danger" v-if="error">{{ error }}</div>
+				</transition>
+				<transition name="fade">
+					<MemberList
+						:members="favoris"
+						v-if="favoris && favoris.length != 0" />
+				</transition>
+				<transition name="fade">
+					<div
+						class="alert alert-info"
+						v-if="!$apollo.queries.favoris.loading && favoris.length == 0">
+						{{ $t("favoris.none") }}
+					</div>
+				</transition>
+			</div>
+		</div>
+	</main>
+</template>
+
+<script>
+import { FAVORIS } from "@/graphql/queries.js"
+
+export default {
+	data() {
+		return {
+			breadcrumb: [
+				{
+					text: this.$t("accueil"),
+					to: "/"
+				},
+				{
+					text: this.$t("favoris.title"),
+					active: true
+				}
+			],
+			error: null
+		}
+	},
+	// local functions. You can use :
+	// {{ myFunction() }} in template if a value is returned
+	// - this.myFunction everywhere in the page but not in arrows functions
+	// - @event="myFunction" on Vue eventHandlers
+	// methods: {
+	//   myFunction() {
+
+	//   }
+	// },
+	// For computed values. Use {{ myComputedValue }} in the template
+	// computed: {
+	//   myComputedValue : function() {
+	//     return this.var * 3
+	//   }
+	// },
+	apollo: {
+		favoris: {
+			query: FAVORIS,
+			variables() {
+				return { group: JSON.parse(localStorage.favourites) }
+			},
+			update(data) {
+				let retour = []
+				for (let i = 0; i < data.filterGroup.selected.length; i++) {
+					retour[i] = data.filterGroup.selected[i].id
+				}
+
+				return retour
+			},
+			error(err) {
+				this.error = err.message
+			}
+		}
+	},
+	nuxtI18n: {
+		paths: {
+			fr: "/favoris",
+			en: "/favourites",
+			es: "/favoritos"
+		}
+	},
+	mounted() {
+		$nuxt.$emit("changeRoute", this.breadcrumb)
+	}
+}
+</script>
-- 
GitLab