PortfolioSettings.vue 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  1. <template>
  2. <div class="portfolio-settings px-3">
  3. <div v-if="loading" class="d-flex justify-content-center align-items-center py-5">
  4. <b-spinner variant="primary" />
  5. </div>
  6. <div v-else class="row justify-content-center mb-5 pb-5">
  7. <div class="col-12 col-md-8 bg-dark py-2 rounded">
  8. <ul class="nav nav-pills nav-fill">
  9. <li v-for="(tab, index) in tabs" class="nav-item" :class="{ disabled: index !== 0 && !settings.active}">
  10. <span v-if="index !== 0 && !settings.active" class="nav-link">{{ tab }}</span>
  11. <a v-else class="nav-link" :class="{ active: tab === tabIndex }" href="#" @click.prevent="toggleTab(tab)">{{ tab }}</a>
  12. </li>
  13. </ul>
  14. </div>
  15. <transition name="slide-fade">
  16. <div v-if="tabIndex === 'Configure'" class="col-12 col-md-8 bg-dark mt-3 py-2 rounded" key="0">
  17. <div v-if="!user.statuses_count" class="alert alert-danger">
  18. <p class="mb-0 small font-weight-bold">You don't have any public posts, once you share public posts you can enable your portfolio.</p>
  19. </div>
  20. <div class="d-flex justify-content-between align-items-center py-2">
  21. <div class="setting-label">
  22. <p class="lead mb-0">Portfolio Enabled</p>
  23. <p class="small mb-0 text-muted">You must enable your portfolio before you or anyone can view it.</p>
  24. </div>
  25. <div class="setting-switch mt-n1">
  26. <b-form-checkbox v-model="settings.active" name="check-button" size="lg" switch :disabled="!user.statuses_count" />
  27. </div>
  28. </div>
  29. <hr>
  30. <div class="d-flex justify-content-between align-items-center py-2">
  31. <div class="setting-label" style="max-width: 50%;">
  32. <p class="mb-0">Portfolio Source</p>
  33. <p class="small mb-0 text-muted">Choose how you want to populate your portfolio, select Most Recent posts to automatically update your portfolio with recent posts or Curated Posts to select specific posts for your portfolio.</p>
  34. </div>
  35. <div class="ml-3">
  36. <b-form-select v-model="settings.profile_source" :options="profileSourceOptions" :disabled="!user.statuses_count" />
  37. </div>
  38. </div>
  39. </div>
  40. <div v-else-if="tabIndex === 'Curate'" class="col-12 col-md-8 mt-3 py-2 px-0" key="1">
  41. <div v-if="!recentPostsLoaded" class="d-flex align-items-center justify-content-center py-5 my-5">
  42. <div class="text-center">
  43. <div class="spinner-border" role="status">
  44. <span class="sr-only">Loading...</span>
  45. </div>
  46. <p class="text-muted">Loading recent posts...</p>
  47. </div>
  48. </div>
  49. <template v-else>
  50. <div class="mt-n2 mb-4">
  51. <p class="text-muted small">Select up to 24 photos from your 100 most recent posts. You can only select public photo posts, videos are not supported at this time.</p>
  52. <div class="d-flex align-items-center justify-content-between">
  53. <p class="font-weight-bold mb-0">Selected {{ selectedRecentPosts.length }}/24</p>
  54. <div>
  55. <button
  56. class="btn btn-link font-weight-bold mr-3 text-decoration-none"
  57. :disabled="!selectedRecentPosts.length"
  58. @click="clearSelected">
  59. Clear selected
  60. </button>
  61. <button
  62. class="btn btn-primary py-0 font-weight-bold"
  63. style="width: 150px;"
  64. :disabled="!canSaveCurated"
  65. @click="saveCurated()">
  66. <template v-if="!isSavingCurated">Save</template>
  67. <b-spinner v-else small />
  68. </button>
  69. </div>
  70. </div>
  71. </div>
  72. <div class="d-flex justify-content-between align-items-center">
  73. <span @click="recentPostsPrev">
  74. <i :class="prevClass" />
  75. </span>
  76. <div class="row flex-grow-1 mx-2">
  77. <div v-for="(post, index) in recentPosts.slice(rpStart, rpStart + 9)" class="col-12 col-md-4 mb-1 p-1">
  78. <div class="square user-select-none" @click.prevent="toggleRecentPost(post.id)">
  79. <transition name="fade">
  80. <img
  81. :key="post.id"
  82. :src="post.media_attachments[0].url"
  83. width="100%"
  84. height="300"
  85. style="overflow: hidden;object-fit: cover;"
  86. :draggable="false"
  87. class="square-content pr-1">
  88. </transition>
  89. <div v-if="selectedRecentPosts.indexOf(post.id) !== -1" style="position: absolute;right: -5px;bottom:-5px;">
  90. <div class="selected-badge">{{ selectedRecentPosts.indexOf(post.id) + 1 }}</div>
  91. </div>
  92. </div>
  93. </div>
  94. </div>
  95. <span @click="recentPostsNext()">
  96. <i :class="nextClass" />
  97. </span>
  98. </div>
  99. </template>
  100. </div>
  101. <div v-else-if="tabIndex === 'Customize'" class="col-12 col-md-8 mt-3 py-2" key="2">
  102. <div v-for="setting in customizeSettings" class="card bg-dark mb-5">
  103. <div class="card-header">{{ setting.title }}</div>
  104. <div class="list-group bg-dark">
  105. <div v-for="item in setting.items" class="list-group-item">
  106. <div class="d-flex justify-content-between align-items-center py-2">
  107. <div class="setting-label">
  108. <p class="mb-0">{{ item.label }}</p>
  109. <p v-if="item.description" class="small text-muted mb-0">{{ item.description }}</p>
  110. </div>
  111. <div class="setting-switch mt-n1">
  112. <b-form-checkbox
  113. v-model="settings[item.model]"
  114. name="check-button"
  115. size="lg"
  116. switch
  117. :disabled="item.requiredWithTrue && !settings[item.requiredWithTrue]" />
  118. </div>
  119. </div>
  120. </div>
  121. </div>
  122. </div>
  123. <div class="card bg-dark mb-5">
  124. <div class="card-header">Portfolio</div>
  125. <div class="list-group bg-dark">
  126. <div class="list-group-item">
  127. <div class="d-flex justify-content-between align-items-center py-2">
  128. <div class="setting-label">
  129. <p class="mb-0">Layout</p>
  130. </div>
  131. <div>
  132. <b-form-select v-model="settings.profile_layout" :options="profileLayoutOptions" />
  133. </div>
  134. </div>
  135. </div>
  136. </div>
  137. </div>
  138. </div>
  139. <div v-else-if="tabIndex === 'Share'" class="col-12 col-md-8 bg-dark mt-3 py-2 rounded" key="0">
  140. <div class="py-2">
  141. <p class="text-muted">Portfolio URL</p>
  142. <p class="lead mb-0"><a :href="settings.url">{{ settings.url }}</a></p>
  143. </div>
  144. </div>
  145. </transition>
  146. </div>
  147. </div>
  148. </template>
  149. <script type="text/javascript">
  150. export default {
  151. data() {
  152. return {
  153. loading: true,
  154. tabIndex: "Configure",
  155. tabs: [
  156. "Configure",
  157. "Customize",
  158. "View Portfolio"
  159. ],
  160. user: undefined,
  161. settings: undefined,
  162. recentPostsLoaded: false,
  163. rpStart: 0,
  164. recentPosts: [],
  165. recentPostsPage: undefined,
  166. selectedRecentPosts: [],
  167. isSavingCurated: false,
  168. canSaveCurated: false,
  169. customizeSettings: [],
  170. profileSourceOptions: [
  171. { value: null, text: 'Please select an option', disabled: true },
  172. { value: 'recent', text: 'Most recent posts' },
  173. ],
  174. profileLayoutOptions: [
  175. { value: null, text: 'Please select an option', disabled: true },
  176. { value: 'grid', text: 'Grid' },
  177. { value: 'masonry', text: 'Masonry' },
  178. { value: 'album', text: 'Album' },
  179. ]
  180. }
  181. },
  182. computed: {
  183. prevClass() {
  184. return this.rpStart === 0 ?
  185. "fa fa-arrow-circle-left fa-3x text-dark" :
  186. "fa fa-arrow-circle-left fa-3x text-muted cursor-pointer";
  187. },
  188. nextClass() {
  189. return this.rpStart > (this.recentPosts.length - 9) ?
  190. "fa fa-arrow-circle-right fa-3x text-dark" :
  191. "fa fa-arrow-circle-right fa-3x text-muted cursor-pointer";
  192. },
  193. },
  194. watch: {
  195. settings: {
  196. deep: true,
  197. immediate: true,
  198. handler: function(o, n) {
  199. if(this.loading) {
  200. return;
  201. }
  202. if(!n.show_timestamp) {
  203. this.settings.show_link = false;
  204. }
  205. this.updateSettings();
  206. }
  207. }
  208. },
  209. mounted() {
  210. this.fetchUser();
  211. },
  212. methods: {
  213. fetchUser() {
  214. axios.get('/api/v1/accounts/verify_credentials')
  215. .then(res => {
  216. this.user = res.data;
  217. if(res.data.statuses_count > 0) {
  218. this.profileSourceOptions = [
  219. { value: null, text: 'Please select an option', disabled: true },
  220. { value: 'recent', text: 'Most recent posts' },
  221. { value: 'custom', text: 'Curated posts' },
  222. ];
  223. } else {
  224. setTimeout(() => {
  225. this.settings.active = false;
  226. this.settings.profile_source = 'recent';
  227. this.tabIndex = 'Configure';
  228. }, 1000);
  229. }
  230. })
  231. axios.post(this.apiPath('/api/portfolio/self/settings.json'))
  232. .then(res => {
  233. this.settings = res.data;
  234. this.updateTabs();
  235. if(res.data.metadata && res.data.metadata.posts) {
  236. this.selectedRecentPosts = res.data.metadata.posts;
  237. }
  238. })
  239. .then(() => {
  240. this.initCustomizeSettings();
  241. })
  242. .then(() => {
  243. const url = new URL(window.location);
  244. if(url.searchParams.has('tab')) {
  245. let tab = url.searchParams.get('tab');
  246. let tabs = this.settings.profile_source === 'custom' ?
  247. ['curate', 'customize', 'share'] :
  248. ['customize', 'share'];
  249. if(tabs.indexOf(tab) !== -1) {
  250. this.toggleTab(tab.slice(0, 1).toUpperCase() + tab.slice(1));
  251. }
  252. }
  253. })
  254. .then(() => {
  255. setTimeout(() => {
  256. this.loading = false;
  257. }, 500);
  258. })
  259. },
  260. apiPath(path) {
  261. return path;
  262. },
  263. toggleTab(idx) {
  264. if(idx === 'Curate' && !this.recentPostsLoaded) {
  265. this.loadRecentPosts();
  266. }
  267. this.tabIndex = idx;
  268. this.rpStart = 0;
  269. if(idx == 'Configure') {
  270. const url = new URL(window.location);
  271. url.searchParams.delete('tab');
  272. window.history.pushState({}, '', url);
  273. } else if (idx == 'View Portfolio') {
  274. this.tabIndex = 'Configure';
  275. window.location.href = `https://${window._portfolio.domain}${window._portfolio.path}/${this.user.username}`;
  276. return;
  277. } else {
  278. const url = new URL(window.location);
  279. url.searchParams.set('tab', idx.toLowerCase());
  280. window.history.pushState({}, '', url);
  281. }
  282. },
  283. updateTabs() {
  284. if(this.settings.profile_source === 'custom') {
  285. this.tabs = [
  286. "Configure",
  287. "Curate",
  288. "Customize",
  289. "View Portfolio"
  290. ];
  291. } else {
  292. this.tabs = [
  293. "Configure",
  294. "Customize",
  295. "View Portfolio"
  296. ];
  297. }
  298. },
  299. updateSettings() {
  300. axios.post(this.apiPath('/api/portfolio/self/update-settings.json'), this.settings)
  301. .then(res => {
  302. this.updateTabs();
  303. this.$bvToast.toast(`Your settings have been successfully updated!`, {
  304. variant: 'dark',
  305. title: 'Settings Updated',
  306. autoHideDelay: 2000,
  307. appendToast: false
  308. })
  309. })
  310. },
  311. loadRecentPosts() {
  312. axios.get('/api/v1/accounts/' + this.user.id + '/statuses?only_media=1&media_types=photo&limit=100')
  313. .then(res => {
  314. if(res.data.length) {
  315. this.recentPosts = res.data.filter(p => p.visibility === "public");
  316. }
  317. })
  318. .then(() => {
  319. setTimeout(() => {
  320. this.recentPostsLoaded = true;
  321. }, 500);
  322. })
  323. },
  324. toggleRecentPost(id) {
  325. if(this.selectedRecentPosts.indexOf(id) == -1) {
  326. if(this.selectedRecentPosts.length === 24) {
  327. return;
  328. }
  329. this.selectedRecentPosts.push(id);
  330. } else {
  331. this.selectedRecentPosts = this.selectedRecentPosts.filter(i => i !== id);
  332. }
  333. this.canSaveCurated = true;
  334. },
  335. recentPostsPrev() {
  336. if(this.rpStart === 0) {
  337. return;
  338. }
  339. this.rpStart = this.rpStart - 9;
  340. },
  341. recentPostsNext() {
  342. if(this.rpStart > (this.recentPosts.length - 9)) {
  343. return;
  344. }
  345. this.rpStart = this.rpStart + 9;
  346. },
  347. clearSelected() {
  348. this.selectedRecentPosts = [];
  349. },
  350. saveCurated() {
  351. this.isSavingCurated = true;
  352. event.currentTarget?.blur();
  353. axios.post('/api/portfolio/self/curated.json', {
  354. ids: this.selectedRecentPosts
  355. })
  356. .then(res => {
  357. this.isSavingCurated = false;
  358. this.$bvToast.toast(`Your curated posts have been updated!`, {
  359. variant: 'dark',
  360. title: 'Portfolio Updated',
  361. autoHideDelay: 2000,
  362. appendToast: false
  363. })
  364. })
  365. .catch(err => {
  366. this.isSavingCurated = false;
  367. this.$bvToast.toast(`An error occured while attempting to update your portfolio, please try again later and contact an admin if this problem persists.`, {
  368. variant: 'dark',
  369. title: 'Error',
  370. autoHideDelay: 2000,
  371. appendToast: false
  372. })
  373. })
  374. },
  375. initCustomizeSettings() {
  376. this.customizeSettings = [
  377. {
  378. title: "Post Settings",
  379. items: [
  380. {
  381. label: "Show Captions",
  382. model: "show_captions"
  383. },
  384. {
  385. label: "Show License",
  386. model: "show_license"
  387. },
  388. {
  389. label: "Show Location",
  390. model: "show_location"
  391. },
  392. {
  393. label: "Show Timestamp",
  394. model: "show_timestamp"
  395. },
  396. {
  397. label: "Link to Post",
  398. description: "Add link to timestamp to view the original post url, requires show timestamp to be enabled",
  399. model: "show_link",
  400. requiredWithTrue: "show_timestamp"
  401. }
  402. ]
  403. },
  404. {
  405. title: "Profile Settings",
  406. items: [
  407. {
  408. label: "Show Avatar",
  409. model: "show_avatar"
  410. },
  411. {
  412. label: "Show Bio",
  413. model: "show_bio"
  414. }
  415. ]
  416. },
  417. ]
  418. }
  419. }
  420. }
  421. </script>