'
39 | ].join('\n');
40 | }).join('\n');
41 | }
42 |
43 | // used by the "backup" feature for rendering contacts in JSON
44 | function appendJsonEntries(div, entries) {
45 | div.innerHTML = div.innerHTML + entries.map(function(entry){
46 | var fields = {};
47 | Object.keys(entry).map(function(field) {
48 | if (entry[field].$t) {
49 | fields[field] = entry[field].$t;
50 | }
51 | });
52 | return JSON.stringify(entry, null, ' ') + '\n';
53 | }).join('');
54 | }
55 |
56 | // used for displaying contacts on the page
57 | function makeAppender(div, appendFct) {
58 | div.innerHTML = '';
59 | return function (json) {
60 | if (!json) {
61 | console.info('done! :-)');
62 | } else {
63 | console.log('=>', json.feed.entry);
64 | appendFct(div, json.feed.entry || []);
65 | }
66 | };
67 | }
68 |
69 | // binds functions to UI elements
70 | function bindUI(token) {
71 | var resultsDiv = document.getElementById('results');
72 | var exportDiv = document.getElementById('export');
73 | document.getElementById('logged').style.display = 'block';
74 | document.getElementById('btnFetchAll').onclick = function(){
75 | fetchAllContacts(token, makeAppender(resultsDiv, appendEntries.bind(token)));
76 | };
77 | document.getElementById('btnBackup').onclick = function(){
78 | backupAllContacts(token, makeAppender(exportDiv, appendJsonEntries));
79 | };
80 | document.getElementById('search').onsubmit = function(evt) {
81 | evt.preventDefault();
82 | var q = document.getElementById('query').value;
83 | searchFullContacts(token, q, makeAppender(resultsDiv, appendEntries.bind(token)));
84 | };
85 | }
86 |
87 | // inits the contacts API and UI on page load
88 | window.onload = function() {
89 | console.log('√ onload');
90 | document.getElementById('btnRegisterProtocol').onclick = function() {
91 | var url = window.location.href.replace(/contacts\.html.*/, 'fdupdate.html') + '?fdupdate=%s';
92 | console.log('btnRegisterProtocol:', url);
93 | // /!\ this may not work from localhost
94 | navigator.registerProtocolHandler('web+fdupdate', url, 'Freelance Directory Update');
95 | }
96 | auth(function(err, token){
97 | if (err) {
98 | console.error('auth =>', err);
99 | // TODO: redirect to home page, for login, or at least display feedback
100 | } else {
101 | console.log('√ auth', token);
102 | bindUI(token);
103 | }
104 | });
105 | };
106 |
107 | })();
108 |
--------------------------------------------------------------------------------
/google-contacts.js:
--------------------------------------------------------------------------------
1 | var CLIENT_ID = '847367303310-1cda1v65gotbpoqjehmhcc21dofjc00q.apps.googleusercontent.com';
2 | // from https://console.developers.google.com/apis/credentials?project=open-1365
3 |
4 | function throwError(err) {
5 | console.error(err);
6 | throw err;
7 | }
8 |
9 | function authPopup(callback) {
10 | var config = {
11 | 'client_id': CLIENT_ID,
12 | 'scope': 'https://www.google.com/m8/feeds'
13 | };
14 | gapi.auth.authorize(config, function(res) {
15 | console.log('gapi.auth.authorize =>', res);
16 | if (res.error) {
17 | alert(res.error);
18 | window.location.href = window.location.href.substr(0, window.location.href.lastIndexOf('/') + 1);
19 | } else {
20 | callback(null, gapi.auth.getToken());
21 | }
22 | });
23 | }
24 |
25 | function auth(callback) {
26 | var config = {
27 | 'immediate': true,
28 | 'client_id': CLIENT_ID,
29 | 'scope': 'https://www.google.com/m8/feeds'
30 | };
31 | gapi.auth.authorize(config, function(res) {
32 | console.log('gapi.auth.authorize =>', res);
33 | if (res.error) {
34 | alert(res.error);
35 | window.location.href = window.location.href.substr(0, window.location.href.lastIndexOf('/') + 1);
36 | } else {
37 | callback(null, gapi.auth.getToken());
38 | }
39 | });
40 | }
41 |
42 | function query(param, callback) {
43 | $.ajax(param).done(callback.bind(null, null)).fail(callback);
44 | }
45 |
46 | function fetchAll(token, opt, handle) {
47 | var projection = opt.projection || 'full';
48 | var path = '/m8/feeds/contacts/default/' + projection;
49 | // jquery-based implementation:
50 | var prefix = 'https://www.google.com';
51 | var url = opt.url || (prefix + path + '?alt=json&max-results=1000&v=3.0&q=' + (opt.q ? encodeURIComponent(opt.q) : ''))
52 | query({
53 | url: url,
54 | dataType: 'json',
55 | data: token
56 | }, function(err, json) {
57 | var next;
58 | json.feed.link.map(function(link){
59 | if (link.rel == 'next') {
60 | next = link.href;
61 | }
62 | });
63 | handle(json);
64 | if (next) {
65 | fetchAll(token, { url: next }, handle);
66 | } else {
67 | handle();
68 | }
69 | });
70 | /*
71 | // this implementation does not require jquery but is a bit slower:
72 | gapi.client.request(opt.url || {
73 | 'path': path,
74 | 'params': {
75 | 'alt': 'json',
76 | 'max-results': 1000,
77 | 'v': '3.0',
78 | 'q': opt.q ? encodeURIComponent(opt.q) : undefined
79 | }
80 | }).then(function(res){
81 | var json = res.result;
82 | var next;
83 | json.feed.link.map(function(link){
84 | if (link.rel == 'next') {
85 | next = link.href;
86 | }
87 | });
88 | handle(json);
89 | if (next) {
90 | fetchAll(token, { url: next }, handle);
91 | } else {
92 | handle();
93 | }
94 | }, throwError);
95 | */
96 | }
97 |
98 | function fetchAllContacts(token, handler) {
99 | fetchAll(token, { projection: 'property-content' }, handler);
100 | }
101 |
102 | function backupAllContacts(token, handler) {
103 | fetchAll(token, { projection: 'full' }, handler);
104 | }
105 |
106 | function searchContacts(token, q, handler) {
107 | fetchAll(token, { projection: 'property-content', q: q }, handler);
108 | }
109 |
110 | function searchFullContacts(token, q, handler) {
111 | fetchAll(token, { projection: 'full', q: q }, handler);
112 | }
113 |
114 | function fetchContact(token, userId, callback) {
115 | var url = '/m8/feeds/contacts/default/full/' + encodeURIComponent('' + (userId || 0));
116 | gapi.client.request({
117 | 'path': url,
118 | 'params': {'alt': 'json'}
119 | }).then(function(json){
120 | callback(null, json.result);
121 | }, callback || throwError);
122 | }
123 |
124 | function updateContact(token, userId, json, callback) {
125 | // use a PUT request to the same URL with updated json data to update the user
126 | // cf https://developers.google.com/google-apps/contacts/v3/?csw=1#Updating
127 | var url = '/m8/feeds/contacts/default/full/' + encodeURIComponent('' + (userId || 0));
128 | // https://developers.google.com/api-client-library/javascript/reference/referencedocs#gapiclientrequest
129 | gapi.client.request({
130 | 'path': url,
131 | 'method': 'PUT',
132 | 'params': {'alt': 'json'},
133 | 'headers': {'ETag': '*', 'If-Match': '*'}, // needed to avoid error 400 Missing resource version ID
134 | 'body': json
135 | }).then(function(json){
136 | callback(null, json.result);
137 | }, callback || throwError);
138 | }
139 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | **WARNING: After you give it your permission, this script edits your Google Contacts**
2 |
3 | **=> [backup your contacts](https://www.google.com/contacts/u/0/?cplus=0#contacts), and use at your own risk!**
4 |
5 | # Freelance Directory Client
6 |
7 | *Problem statement: It's slow and boring to look for relevant (and available) freelancing friends, when we want to forward a mission to them.*
8 |
9 | 
10 |
11 | Freelance Directory Client is a personal contacts directory in which I can add my freelance friends, look for relevant ones based on technologies, and subscribe to their updates of skills, preferences and availability (in order to update my directory). It's based on Google Contacts.
12 |
13 | - You can try it from: [adrienjoly.com/freelance-directory-client](https://adrienjoly.com/freelance-directory-client)
14 | - Sample profile, on GitHub: [adrienjoly/freelance-directory-profile](https://github.com/adrienjoly/freelance-directory-profile) (fork it and put your info!)
15 | - The story behing this project: [Side project #6 - Freelance Directory Client](https://www.getrevue.co/profile/aj-sideprojects/issues/side-project-6-freelance-directory-client-23842)
16 | - Source code: [this repository](https://github.com/adrienjoly/freelance-directory-client)
17 |
18 | ## FEATURES: Use cases
19 |
20 | - Find relevant and available Freelancers quickly from your contacts, based on the stack/skills of the mission that you want to forward **[WORKING]**
21 | - Edit the technical stack / skills, preferences, and availability of your Freelancing contacts, manually **[COMING SOON]**
22 | - Subscribe to your Freelancing contacts, to help you update their info (i.e. stack/skills, prefs, avail.) **[COMING SOON]**
23 | - As a Freelancer, publish your updates (i.e. stack/skills, prefs, avail.), so that your friends can integrate changes into their directory **[WORKING]**
24 |
25 | ## USAGE: Working features
26 |
27 | 1. Display and backup your contacts
28 |
29 | + When opening the page, give permission to access your Google Contacts. (*don't worry, I can't store any personal data because this app has no back-end server!*)
30 | + The "list contacts" button appends all your contacts on the page, to make sure that the app is connected to your Google Contacts account.
31 | + Click "backup contacts" button to generates a JSON export of your contacts, and appends it on the page. Then keep that JSON data safely in a file, just in case. :-)
32 | + Enter a word to search for contacts whose description contain that word.
33 | + Display a contact by id. (e.g. the first one's id is 0)
34 |
35 | 2. Update a contact's description
36 |
37 | + Append "Coucou" to that contact (i.e. use its id)
38 | + Display that contact again, to realize that the app has stored "Coucou" in the corresponding Google Contacts directory. => It's still displayed if you refresh the app.
39 | + You can now search for contacts that contain "Coucou"
40 |
41 | 3. Receive contact updates from external sources
42 |
43 | + Click the "Register protocol" button, so that `web+fdupdate://` URLs are transmitted to this app, at this URL.
44 | + Copy the URL of the "Test custom protocol handler" link.
45 | + Close the app's tab.
46 | + Open the copied URL (`web+fdupdate://...`) in a new tab => the app should open and display the content of the incoming update (a github commit)
47 | + In the future, you will be able to append up-to-date availability information from a freelancing friend into your corresponding Google Contact. This information will be stored in a Github commit, and pushed to you by email, in the form of a `web+fdupdate://...` link. :-)
48 |
49 | ## PRICING: Is this free to use?
50 |
51 | Freelance-directory-client is free, and will always be free to use, for several reasons:
52 |
53 | - As a tool that promotes sharing between freelancers and eases the process of our clients to find relevant talent, I'm willing to encourage my peers to use it, without financial barrier.
54 | - So far, I don't need to setup and maintain a server/back-end infrastructure for this app, so it costs me nothing (but a bit of time to maintain the code).
55 |
56 | So I'm happy to offer this product for free! :-)
57 |
58 | ...And, if I ever change my mind, you will still be able to fork this repo to keep using it for free!
59 |
60 | ## API: Syntax of custom update URLs
61 |
62 | Syntax: `web+fdupdate:/github.com///commit/`
63 |
64 | Sample URL: `web+fdupdate:adri@whyd.com/github.com/adrienjoly/freelance-directory-profile/commit/2987b22c22df618d464af5e44d0d5c32d28e21c2`
65 |
66 | The sample URL above informs `freelance-directory-client` that:
67 |
68 | - your friend Adrien whose email address is `adri@whyd.com` has updated his freelancing info;
69 | - this update is stored in a commit that can be found there: [github.com/adrienjoly/freelance-directory-profile/commit/2987b22c22df618d464af5e44d0d5c32d28e21c2](http://github.com/adrienjoly/freelance-directory-profile/commit/2987b22c22df618d464af5e44d0d5c32d28e21c2), which contains:
70 |
71 | ```
72 |
73 | #nodejs #vuejs #reactjs #mongodb developer.
74 | rate: 120€/hour
75 | availability: half-time, but only for collaborations (not for paid missions), cf adrienjoly.com/now
76 | preferences: remote work only, cf contact page of adrienjoly.com
77 |
78 | ```
79 |
80 | For instance, this update contains:
81 |
82 | - technologies that Adrien works on professionally, in the form of hashtags;
83 | - Adrien's current hourly rate;
84 | - his current availability;
85 | - and his current preferences.
86 |
87 | **If you want to publish your freelance profile, fork the [freelance-directory-profile](https://github.com/adrienjoly/freelance-directory-profile) repository in your own Github account, and fill it with your own info.**
88 |
89 | ## ROADMAP: Next steps
90 |
91 | - Actually store updates from custom URLs into the corresponding contact
92 | - Design an actual UI for the product
93 | - Design+copywriting: make an explanatory landing page with a Google Connect button
94 |
95 | ## SETUP: Forking instructions
96 |
97 | - After forking, don't forget to set your own Google Client id in `google-contacts.js`
98 |
--------------------------------------------------------------------------------