diff --git a/app/components/gh-datetime-input.js b/app/components/gh-datetime-input.js index de4e34d502..f24e594051 100644 --- a/app/components/gh-datetime-input.js +++ b/app/components/gh-datetime-input.js @@ -4,7 +4,11 @@ import boundOneWay from 'ghost/utils/bound-one-way'; import {formatDate} from 'ghost/utils/date-formatting'; import {invokeAction} from 'ember-invoke-action'; -const {Component} = Ember; +const { + Component, + RSVP, + inject: {service} +} = Ember; export default Component.extend(TextInputMixin, { tagName: 'span', @@ -14,15 +18,21 @@ export default Component.extend(TextInputMixin, { inputClass: null, inputId: null, inputName: null, + timeZone: service(), didReceiveAttrs() { - let datetime = this.get('datetime') || moment(); + let promises = { + datetime: RSVP.resolve(this.get('datetime') || moment.utc()), + offset: RSVP.resolve(this.get('timeZone.offset')) + }; if (!this.get('update')) { throw new Error(`You must provide an \`update\` action to \`{{${this.templateName}}}\`.`); } - this.set('datetime', formatDate(datetime)); + RSVP.hash(promises).then((hash) => { + this.set('datetime', formatDate(hash.datetime || moment.utc(), hash.offset)); + }); }, focusOut() { diff --git a/app/components/gh-user-active.js b/app/components/gh-user-active.js index 13e7735bdb..e899d4fcf1 100644 --- a/app/components/gh-user-active.js +++ b/app/components/gh-user-active.js @@ -26,6 +26,6 @@ export default Component.extend({ lastLogin: computed('user.lastLogin', function () { let lastLogin = this.get('user.lastLogin'); - return lastLogin ? lastLogin.fromNow() : '(Never)'; + return lastLogin ? moment(lastLogin).fromNow() : '(Never)'; }) }); diff --git a/app/components/gh-user-invited.js b/app/components/gh-user-invited.js index 45a89c2ccb..24325ca3ce 100644 --- a/app/components/gh-user-invited.js +++ b/app/components/gh-user-invited.js @@ -17,7 +17,7 @@ export default Component.extend({ createdAt: computed('user.createdAt', function () { let createdAt = this.get('user.createdAt'); - return createdAt ? createdAt.fromNow() : ''; + return createdAt ? moment(createdAt).fromNow() : ''; }), actions: { diff --git a/app/controllers/post-settings-menu.js b/app/controllers/post-settings-menu.js index e83e69e4fe..d8100c8383 100644 --- a/app/controllers/post-settings-menu.js +++ b/app/controllers/post-settings-menu.js @@ -31,6 +31,7 @@ export default Controller.extend(SettingsMenuMixin, { notifications: service(), session: service(), slugGenerator: service(), + timeZone: service(), initializeSelectedAuthor: observer('model', function () { return this.get('model.author').then((author) => { @@ -294,48 +295,53 @@ export default Controller.extend(SettingsMenuMixin, { if (this.get('model.isDraft')) { this.set('model.publishedAt', null); } - return; } + this.get('timeZone.offset').then((offset) => { + let newPublishedAt = parseDateString(userInput, offset); + let publishedAt = moment.utc(this.get('model.publishedAt')); + let errMessage = ''; + + // Clear previous errors + this.get('model.errors').remove('post-setting-date'); + + // Validate new Published date + if (!newPublishedAt.isValid()) { + errMessage = 'Published Date must be a valid date with format: ' + + 'DD MMM YY @ HH:mm (e.g. 6 Dec 14 @ 15:00)'; + } - let newPublishedAt = parseDateString(userInput); - let publishedAt = moment(this.get('model.publishedAt')); - let errMessage = ''; - - // Clear previous errors - this.get('model.errors').remove('post-setting-date'); + // Date is a valid date, so now make it UTC + newPublishedAt = moment.utc(newPublishedAt); - // Validate new Published date - if (!newPublishedAt.isValid()) { - errMessage = 'Published Date must be a valid date with format: ' + - 'DD MMM YY @ HH:mm (e.g. 6 Dec 14 @ 15:00)'; - } else if (newPublishedAt.diff(new Date(), 'h') > 0) { - errMessage = 'Published Date cannot currently be in the future.'; - } + if (newPublishedAt.diff(moment.utc(new Date()), 'hours', true) > 0) { + errMessage = 'Published Date cannot currently be in the future.'; + } - // If errors, notify and exit. - if (errMessage) { - this.get('model.errors').add('post-setting-date', errMessage); - return; - } + // If errors, notify and exit. + if (errMessage) { + this.get('model.errors').add('post-setting-date', errMessage); + return; + } - // Validation complete, update the view - this.set('model.publishedAt', newPublishedAt); + // Do nothing if the user didn't actually change the date + if (publishedAt && publishedAt.isSame(newPublishedAt)) { + return; + } - // Don't save the date if the user didn't actually changed the date - if (publishedAt && publishedAt.isSame(newPublishedAt)) { - return; - } + // Validation complete + this.set('model.publishedAt', newPublishedAt); - // If this is a new post. Don't save the model. Defer the save - // to the user pressing the save button - if (this.get('model.isNew')) { - return; - } + // If this is a new post. Don't save the model. Defer the save + // to the user pressing the save button + if (this.get('model.isNew')) { + return; + } - this.get('model').save().catch((errors) => { - this.showErrors(errors); - this.get('model').rollbackAttributes(); + this.get('model').save().catch((errors) => { + this.showErrors(errors); + this.get('model').rollbackAttributes(); + }); }); }, diff --git a/app/controllers/settings/general.js b/app/controllers/settings/general.js index 92938e2ea7..5cd4b1dee4 100644 --- a/app/controllers/settings/general.js +++ b/app/controllers/settings/general.js @@ -15,10 +15,13 @@ export default Controller.extend(SettingsSaveMixin, { showUploadLogoModal: false, showUploadCoverModal: false, + availableTimezones: null, + notifications: service(), config: service(), _scratchFacebook: null, _scratchTwitter: null, + clock: service(), selectedTheme: computed('model.activeTheme', 'themes', function () { let activeTheme = this.get('model.activeTheme'); @@ -34,6 +37,15 @@ export default Controller.extend(SettingsSaveMixin, { return selectedTheme; }), + selectedTimezone: computed('model.activeTimezone', 'availableTimezones', function () { + let activeTimezone = this.get('model.activeTimezone'); + let availableTimezones = this.get('availableTimezones'); + + return availableTimezones + .filterBy('name', activeTimezone) + .get('firstObject'); + }), + logoImageSource: computed('model.logo', function () { return this.get('model.logo') || ''; }), @@ -42,6 +54,12 @@ export default Controller.extend(SettingsSaveMixin, { return this.get('model.cover') || ''; }), + localTime: computed('selectedTimezone', 'clock.second', function () { + let timezone = this.get('selectedTimezone.name'); + this.get('clock.second'); + return timezone ? moment().tz(timezone).format('HH:mm:ss') : moment().utc().format('HH:mm:ss'); + }), + isDatedPermalinks: computed('model.permalinks', { set(key, value) { this.set('model.permalinks', value ? '/:year/:month/:day/:slug/' : '/:slug/'); @@ -82,7 +100,6 @@ export default Controller.extend(SettingsSaveMixin, { save() { let notifications = this.get('notifications'); let config = this.get('config'); - return this.get('model').save().then((model) => { config.set('blogTitle', model.get('title')); @@ -111,7 +128,9 @@ export default Controller.extend(SettingsSaveMixin, { setTheme(theme) { this.set('model.activeTheme', theme.name); }, - + setTimezone(timezone) { + this.set('model.activeTimezone', timezone.name); + }, toggleUploadCoverModal() { this.toggleProperty('showUploadCoverModal'); }, diff --git a/app/helpers/gh-format-timeago.js b/app/helpers/gh-format-timeago.js index 97bf5d18c4..fef3632d97 100644 --- a/app/helpers/gh-format-timeago.js +++ b/app/helpers/gh-format-timeago.js @@ -2,14 +2,18 @@ import Ember from 'ember'; const {Helper} = Ember; -export default Helper.helper(function (params) { +export function timeAgo(params) { if (!params || !params.length) { return; } - let [timeago] = params; + let utc = moment.utc(); - return moment(timeago).fromNow(); + return moment(timeago).from(utc); +} + +export default Helper.helper(function (params) { + return timeAgo(params); // stefanpenner says cool for small number of timeagos. // For large numbers moment sucks => single Ember.Object based clock better // https://github.com/manuelmitasch/ghost-admin-ember-demo/commit/fba3ab0a59238290c85d4fa0d7c6ed1be2a8a82e#commitcomment-5396524 diff --git a/app/mirage/config.js b/app/mirage/config.js index 742bd4491d..6378e85a76 100644 --- a/app/mirage/config.js +++ b/app/mirage/config.js @@ -207,6 +207,27 @@ export function testConfig() { return response; }); + this.get('/posts/:id', function (db, request) { + let {id} = request.params; + let post = db.posts.find(id); + + return { + posts: [post] + }; + }); + + this.put('/posts/:id/', function (db, request) { + let {id} = request.params; + let [attrs] = JSON.parse(request.requestBody).posts; + delete attrs.id; + + let post = db.posts.update(id, attrs); + + return { + posts: [post] + }; + }); + this.del('/posts/:id/', function (db, request) { db.posts.remove(request.params.id); @@ -264,6 +285,16 @@ export function testConfig() { return {}; }); + /* Configuration -------------------------------------------------------- */ + + this.get('/configuration/timezones/', function (db) { + return { + configuration: [{ + timezones: db.timezones + }] + }; + }); + /* Slugs ---------------------------------------------------------------- */ this.get('/slugs/post/:slug/', function (db, request) { diff --git a/app/mirage/factories/post.js b/app/mirage/factories/post.js index d61a4c3ec5..89d1a2dc4b 100644 --- a/app/mirage/factories/post.js +++ b/app/mirage/factories/post.js @@ -1,5 +1,5 @@ /* jscs:disable */ -import Mirage from 'ember-cli-mirage'; +import Mirage, {faker} from 'ember-cli-mirage'; export default Mirage.Factory.extend({ uuid(i) { return `post-${i}`; }, @@ -10,14 +10,15 @@ export default Mirage.Factory.extend({ image(i) { return `/content/images/2015/10/post-${i}.jpg`; }, featured() { return false; }, page() { return false; }, - status(i) { return `/content/images/2015/10/post-${i}.jpg`; }, + status(i) { return faker.list.cycle('draft', 'published')(i); }, meta_description(i) { return `Meta description for post ${i}.`; }, meta_title(i) { return `Meta Title for post ${i}`; }, author_id() { return 1; }, updated_at() { return '2015-10-19T16:25:07.756Z'; }, updated_by() { return 1; }, - published_at() { return '2015-10-19T16:25:07.756Z'; }, + published_at() { return '2015-12-19T16:25:07.000Z'; }, published_by() { return 1; }, created_at() { return '2015-09-11T09:44:29.871Z'; }, - created_by() { return 1; } + created_by() { return 1; }, + tags() { return []; } }); diff --git a/app/mirage/fixtures/settings.js b/app/mirage/fixtures/settings.js index 472c9a00e3..1e62a9c233 100644 --- a/app/mirage/fixtures/settings.js +++ b/app/mirage/fixtures/settings.js @@ -201,6 +201,17 @@ export default [ uuid: '5130441f-e4c7-4750-9692-a22d841ab049', value: '@test' }, + { + created_at: '2015-09-11T09:44:30.810Z', + created_by: 1, + id: 16, + key: 'activeTimezone', + type: 'blog', + updated_at: '2015-09-23T13:32:49.868Z', + updated_by: 1, + uuid: '310c9169-9613-48b0-8bc4-d1e1c9be85b8', + value: 'Europe/Dublin' + }, { key: 'availableThemes', value: [ diff --git a/app/mirage/fixtures/timezones.js b/app/mirage/fixtures/timezones.js new file mode 100644 index 0000000000..e38f119bf7 --- /dev/null +++ b/app/mirage/fixtures/timezones.js @@ -0,0 +1,327 @@ +export default [ + { + name: 'Pacific/Pago_Pago', + label: '(GMT -11:00) Midway Island, Samoa', + offset: -660 + }, + { + name: 'Pacific/Honolulu', + label: '(GMT -10:00) Hawaii', + offset: -600 + }, + { + name: 'America/Anchorage', + label: '(GMT -9:00) Alaska', + offset: -540 + }, + { + name: 'America/Tijuana', + label: '(GMT -8:00) Chihuahua, La Paz, Mazatlan', + offset: -480 + }, + { + name: 'America/Los_Angeles', + label: '(GMT -8:00) Pacific Time (US & Canada); Tijuana', + offset: -480 + }, + { + name: 'America/Phoenix', + label: '(GMT -7:00) Arizona', + offset: -420 + }, + { + name: 'America/Denver', + label: '(GMT -7:00) Mountain Time (US & Canada)', + offset: -420 + }, + { + name: 'America/Costa_Rica', + label: '(GMT -6:00) Central America', + offset: -360 + }, + { + name: 'America/Chicago', + label: '(GMT -6:00) Central Time (US & Canada)', + offset: -360 + }, + { + name: 'America/Mexico_City', + label: '(GMT -6:00) Guadalajara, Mexico City, Monterrey', + offset: -360 + }, + { + name: 'America/Regina', + label: '(GMT -6:00) Saskatchewan', + offset: -360 + }, + { + name: 'America/Bogota', + label: '(GMT -5:00) Bogota, Lima, Quito', + offset: -300 + }, + { + name: 'America/New_York', + label: '(GMT -5:00) Eastern Time (US & Canada)', + offset: -300 + }, + { + name: 'America/Fort_Wayne', + label: '(GMT -5:00) Indiana (East)', + offset: -300 + }, + { + name: 'America/Caracas', + label: '(GMT -4:30) Caracas, La Paz', + offset: -270 + }, + { + name: 'America/Halifax', + label: '(GMT -4:00) Atlantic Time (Canada); Brasilia, Greenland', + offset: -240 + }, + { + name: 'America/St_Johns', + label: '(GMT -3:30) Newfoundland', + offset: -210 + }, + { + name: 'America/Argentina/Buenos_Aires', + label: '(GMT -3:00) Buenos Aires, Georgetown', + offset: -180 + }, + { + name: 'America/Santiago', + label: '(GMT -3:00) Santiago', + offset: -180 + }, + { + name: 'America/Noronha', + label: '(GMT -2:00) Fernando de Noronha', + offset: -120 + }, + { + name: 'Atlantic/Azores', + label: '(GMT -1:00) Azores', + offset: -60 + }, + { + name: 'Atlantic/Cape_Verde', + label: '(GMT -1:00) Cape Verde Is.', + offset: -60 + }, + { + name: 'Africa/Casablanca', + label: '(GMT) Casablanca, Monrovia', + offset: 0 + }, + { + name: 'Europe/Dublin', + label: '(GMT) Greenwich Mean Time : Dublin, Edinburgh, London', + offset: 0 + }, + { + name: 'Europe/Amsterdam', + label: '(GMT +1:00) Amsterdam, Berlin, Rome, Stockholm, Vienna', + offset: 60 + }, + { + name: 'Europe/Prague', + label: '(GMT +1:00) Belgrade, Bratislava, Budapest, Prague', + offset: 60 + }, + { + name: 'Europe/Paris', + label: '(GMT +1:00) Brussels, Copenhagen, Madrid, Paris', + offset: 60 + }, + { + name: 'Europe/Warsaw', + label: '(GMT +1:00) Sarajevo, Skopje, Warsaw, Zagreb', + offset: 60 + }, + { + name: 'Africa/Lagos', + label: '(GMT +1:00) West Central Africa', + offset: 60 + }, + { + name: 'Europe/Istanbul', + label: '(GMT +2:00) Athens, Beirut, Bucharest, Istanbul', + offset: 120 + }, + { + name: 'Africa/Cairo', + label: '(GMT +2:00) Cairo, Egypt', + offset: 120 + }, + { + name: 'Africa/Maputo', + label: '(GMT +2:00) Harare', + offset: 120 + }, + { + name: 'Europe/Kiev', + label: '(GMT +2:00) Helsinki, Kiev, Riga, Sofia, Tallinn, Vilnius', + offset: 120 + }, + { + name: 'Asia/Jerusalem', + label: '(GMT +2:00) Jerusalem', + offset: 120 + }, + { + name: 'Africa/Johannesburg', + label: '(GMT +2:00) Pretoria', + offset: 120 + }, + { + name: 'Asia/Baghdad', + label: '(GMT +3:00) Baghdad', + offset: 180 + }, + { + name: 'Asia/Riyadh', + label: '(GMT +3:00) Kuwait, Nairobi, Riyadh', + offset: 180 + }, + { + name: 'Asia/Tehran', + label: '(GMT +3:30) Tehran', + offset: 210 + }, + { + name: 'Asia/Dubai', + label: '(GMT +4:00) Abu Dhabi, Muscat', + offset: 240 + }, + { + name: 'Asia/Baku', + label: '(GMT +4:00) Baku, Tbilisi, Yerevan', + offset: 240 + }, + { + name: 'Europe/Moscow', + label: '(GMT +4:00) Moscow, St. Petersburg, Volgograd', + offset: 240 + }, + { + name: 'Asia/Kabul', + label: '(GMT +4:30) Kabul', + offset: 270 + }, + { + name: 'Asia/Karachi', + label: '(GMT +5:00) Islamabad, Karachi, Tashkent', + offset: 300 + }, + { + name: 'Asia/Kolkata', + label: '(GMT +5:30) Chennai, Calcutta, Mumbai, New Delhi', + offset: 330 + }, + { + name: 'Asia/Kathmandu', + label: '(GMT +5:45) Katmandu', + offset: 345 + }, + { + name: 'Asia/Almaty', + label: '(GMT +6:00) Almaty, Novosibirsk', + offset: 360 + }, + { + name: 'Asia/Dhaka', + label: '(GMT +6:00) Astana, Dhaka, Sri Jayawardenepura', + offset: 360 + }, + { + name: 'Asia/Yekaterinburg', + label: '(GMT +6:00) Yekaterinburg', + offset: 360 + }, + { + name: 'Asia/Rangoon', + label: '(GMT +6:30) Rangoon', + offset: 390 + }, + { + name: 'Asia/Bangkok', + label: '(GMT +7:00) Bangkok, Hanoi, Jakarta', + offset: 420 + }, + { + name: 'Asia/Hong_Kong', + label: '(GMT +8:00) Beijing, Chongqing, Hong Kong, Urumqi', + offset: 480 + }, + { + name: 'Asia/Krasnoyarsk', + label: '(GMT +8:00) Krasnoyarsk', + offset: 480 + }, + { + name: 'Asia/Singapore', + label: '(GMT +8:00) Kuala Lumpur, Perth, Singapore, Taipei', + offset: 480 + }, + { + name: 'Asia/Irkutsk', + label: '(GMT +9:00) Irkutsk, Ulaan Bataar', + offset: 540 + }, + { + name: 'Asia/Tokyo', + label: '(GMT +9:00) Osaka, Sapporo, Tokyo', + offset: 540 + }, + { + name: 'Asia/Seoul', + label: '(GMT +9:00) Seoul', + offset: 540 + }, + { + name: 'Australia/Darwin', + label: '(GMT +9:30) Darwin', + offset: 570 + }, + { + name: 'Australia/Brisbane', + label: '(GMT +10:00) Brisbane, Guam, Port Moresby', + offset: 600 + }, + { + name: 'Asia/Yakutsk', + label: '(GMT +10:00) Yakutsk', + offset: 600 + }, + { + name: 'Australia/Adelaide', + label: '(GMT +10:30) Adelaide', + offset: 630 + }, + { + name: 'Australia/Sydney', + label: '(GMT +11:00) Canberra, Hobart, Melbourne, Sydney, Vladivostok', + offset: 660 + }, + { + name: 'Pacific/Fiji', + label: '(GMT +12:00) Fiji, Kamchatka, Marshall Is.', + offset: 720 + }, + { + name: 'Pacific/Kwajalein', + label: '(GMT +12:00) International Date Line West', + offset: 720 + }, + { + name: 'Asia/Magadan', + label: '(GMT +12:00) Magadan, Soloman Is., New Caledonia', + offset: 720 + }, + { + name: 'Pacific/Auckland', + label: '(GMT +13:00) Auckland, Wellington', + offset: 780 + } +]; diff --git a/app/models/post.js b/app/models/post.js index a1c094e032..019350c79e 100644 --- a/app/models/post.js +++ b/app/models/post.js @@ -28,11 +28,11 @@ export default Model.extend(ValidationEngine, { metaDescription: attr('string'), author: belongsTo('user', {async: true}), authorId: attr('number'), - updatedAt: attr('moment-date'), + updatedAt: attr('moment-utc'), updatedBy: attr(), - publishedAt: attr('moment-date'), + publishedAt: attr('moment-utc'), publishedBy: belongsTo('user', {async: true}), - createdAt: attr('moment-date'), + createdAt: attr('moment-utc'), createdBy: attr(), tags: hasMany('tag', { embedded: 'always', @@ -42,6 +42,7 @@ export default Model.extend(ValidationEngine, { config: service(), ghostPaths: service(), + timeZone: service(), absoluteUrl: computed('url', 'ghostPaths.url', 'config.blogUrl', function () { let blogUrl = this.get('config.blogUrl'); diff --git a/app/models/role.js b/app/models/role.js index 6d93ba775b..caa97ba1bb 100644 --- a/app/models/role.js +++ b/app/models/role.js @@ -9,8 +9,8 @@ export default Model.extend({ uuid: attr('string'), name: attr('string'), description: attr('string'), - createdAt: attr('moment-date'), - updatedAt: attr('moment-date'), + createdAt: attr('moment-utc'), + updatedAt: attr('moment-utc'), createdBy: attr(), updatedBy: attr(), diff --git a/app/models/setting.js b/app/models/setting.js index 22a1884944..33386a7da7 100644 --- a/app/models/setting.js +++ b/app/models/setting.js @@ -16,6 +16,7 @@ export default Model.extend(ValidationEngine, { permalinks: attr('string'), activeTheme: attr('string'), availableThemes: attr(), + activeTimezone: attr('string', {defaultValue: 'Europe/Dublin'}), ghost_head: attr('string'), ghost_foot: attr('string'), facebook: attr('facebook-url-user'), diff --git a/app/models/subscriber.js b/app/models/subscriber.js index de8a7f90d6..4de582c3c4 100644 --- a/app/models/subscriber.js +++ b/app/models/subscriber.js @@ -13,9 +13,9 @@ export default Model.extend(ValidationEngine, { subscribedUrl: attr('string'), subscribedReferrer: attr('string'), unsubscribedUrl: attr('string'), - unsubscribedAt: attr('moment-date'), - createdAt: attr('moment-date'), - updatedAt: attr('moment-date'), + unsubscribedAt: attr('moment-utc'), + createdAt: attr('moment-utc'), + updatedAt: attr('moment-utc'), createdBy: attr('number'), updatedBy: attr('number'), diff --git a/app/models/tag.js b/app/models/tag.js index ef5f03f7a1..2decf8b107 100644 --- a/app/models/tag.js +++ b/app/models/tag.js @@ -15,8 +15,8 @@ export default Model.extend(ValidationEngine, { metaDescription: attr('string'), image: attr('string'), hidden: attr('boolean'), - createdAt: attr('moment-date'), - updatedAt: attr('moment-date'), + createdAt: attr('moment-utc'), + updatedAt: attr('moment-utc'), createdBy: attr(), updatedBy: attr(), count: attr('raw') diff --git a/app/models/user.js b/app/models/user.js index e3d3dab14f..95ad590180 100644 --- a/app/models/user.js +++ b/app/models/user.js @@ -28,10 +28,10 @@ export default Model.extend(ValidationEngine, { language: attr('string', {defaultValue: 'en_US'}), metaTitle: attr('string'), metaDescription: attr('string'), - lastLogin: attr('moment-date'), - createdAt: attr('moment-date'), + lastLogin: attr('moment-utc'), + createdAt: attr('moment-utc'), createdBy: attr('number'), - updatedAt: attr('moment-date'), + updatedAt: attr('moment-utc'), updatedBy: attr('number'), roles: hasMany('role', { embedded: 'always', diff --git a/app/routes/settings/general.js b/app/routes/settings/general.js index 56e5be0fc4..02a1cdf314 100644 --- a/app/routes/settings/general.js +++ b/app/routes/settings/general.js @@ -1,12 +1,20 @@ +import Ember from 'ember'; import AuthenticatedRoute from 'ghost/routes/authenticated'; import CurrentUserSettings from 'ghost/mixins/current-user-settings'; import styleBody from 'ghost/mixins/style-body'; +const { + RSVP, + inject: {service} +} = Ember; + export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, { titleToken: 'Settings - General', classNames: ['settings-view-general'], + config: service(), + beforeModel() { this._super(...arguments); return this.get('session.user') @@ -15,7 +23,15 @@ export default AuthenticatedRoute.extend(styleBody, CurrentUserSettings, { }, model() { - return this.store.queryRecord('setting', {type: 'blog,theme,private'}); + return RSVP.hash({ + settings: this.store.queryRecord('setting', {type: 'blog,theme,private'}), + availableTimezones: this.get('config.availableTimezones') + }); + }, + + setupController(controller, models) { + controller.set('model', models.settings); + controller.set('availableTimezones', models.availableTimezones); }, actions: { diff --git a/app/services/clock.js b/app/services/clock.js new file mode 100644 index 0000000000..66a88f45f3 --- /dev/null +++ b/app/services/clock.js @@ -0,0 +1,37 @@ +import Ember from 'ember'; + +const { + Service, + run +} = Ember; + +const ONE_SECOND = 1000; + +// Creates a clock service to run intervals. + +export default Service.extend({ + second: null, + minute: null, + hour: null, + + init() { + this.tick(); + }, + + tick() { + let now = moment().utc(); + this.setProperties({ + second: now.seconds(), + minute: now.minutes(), + hour: now.hours() + }); + + if (!Ember.testing) { + run.later(() => { + this.tick(); + }, ONE_SECOND); + } + + } + +}); diff --git a/app/services/config.js b/app/services/config.js index b71b3bc2d8..846dd55a8b 100644 --- a/app/services/config.js +++ b/app/services/config.js @@ -1,6 +1,11 @@ import Ember from 'ember'; -const {Service, _ProxyMixin, computed} = Ember; +const { + Service, + _ProxyMixin, + computed, + inject: {service} +} = Ember; function isNumeric(num) { return Ember.$.isNumeric(num); @@ -25,6 +30,9 @@ function _mapType(val, type) { } export default Service.extend(_ProxyMixin, { + ajax: service(), + ghostPaths: service(), + content: computed(function () { let metaConfigTags = Ember.$('meta[name^="env-"]'); let config = {}; @@ -40,5 +48,17 @@ export default Service.extend(_ProxyMixin, { }); return config; + }), + + availableTimezones: computed(function() { + let timezonesUrl = this.get('ghostPaths.url').api('configuration', 'timezones'); + + return this.get('ajax').request(timezonesUrl).then((configTimezones) => { + let [ timezonesObj ] = configTimezones.configuration; + + timezonesObj = timezonesObj.timezones; + + return timezonesObj; + }); }) }); diff --git a/app/services/time-zone.js b/app/services/time-zone.js new file mode 100644 index 0000000000..aeafca9038 --- /dev/null +++ b/app/services/time-zone.js @@ -0,0 +1,28 @@ +import Ember from 'ember'; + +const { + Service, + computed, + inject: {service} +} = Ember; + +export default Service.extend({ + store: service(), + + _parseTimezones(settings) { + let activeTimezone = settings.get('activeTimezone'); + return activeTimezone; + }, + + _settings: computed(function () { + let store = this.get('store'); + return store.queryRecord('setting', {type: 'blog,theme,private'}); + }), + + offset: computed('_settings.activeTimezone', function () { + return this.get('_settings').then((settings) => { + return this._parseTimezones(settings); + }); + }) + +}); diff --git a/app/templates/settings/general.hbs b/app/templates/settings/general.hbs index 495791a206..b06675cbad 100644 --- a/app/templates/settings/general.hbs +++ b/app/templates/settings/general.hbs @@ -113,6 +113,22 @@ {{/gh-form-group}} +
+ + + {{gh-select-native + id="activeTimezone" + name="general[activeTimezone]" + content=availableTimezones + optionValuePath="name" + optionLabelPath="label" + selection=selectedTimezone + action="setTimezone" + }} + +

The local time here is currently {{localTime}}

+
+