123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670 |
- <template>
- <div class="portfolio-settings px-3">
- <div v-if="loading" class="d-flex justify-content-center align-items-center py-5">
- <b-spinner variant="primary" />
- </div>
- <div v-else class="row justify-content-center mb-5 pb-5">
- <div class="col-12 col-md-8 bg-dark py-2 rounded">
- <ul class="nav nav-pills nav-fill">
- <li v-for="(tab, index) in tabs" class="nav-item" :class="{ disabled: index !== 0 && !settings.active}">
- <span v-if="index !== 0 && !settings.active" class="nav-link">{{ tab }}</span>
- <a v-else class="nav-link" :class="{ active: tab === tabIndex }" href="#" @click.prevent="toggleTab(tab)">{{ tab }}</a>
- </li>
- </ul>
- </div>
- <transition name="slide-fade">
- <div v-if="tabIndex === 'Configure'" class="col-12 col-md-8 bg-dark mt-3 py-2 rounded" key="0">
- <div v-if="!user.statuses_count" class="alert alert-danger">
- <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>
- </div>
- <div class="d-flex justify-content-between align-items-center py-2">
- <div class="setting-label">
- <p class="lead mb-0">Portfolio Enabled</p>
- <p class="small mb-0 text-muted">You must enable your portfolio before you or anyone can view it.</p>
- </div>
- <div class="setting-switch mt-n1">
- <b-form-checkbox v-model="settings.active" name="check-button" size="lg" switch :disabled="!user.statuses_count" />
- </div>
- </div>
- <hr>
- <div class="d-flex justify-content-between align-items-center py-2">
- <div class="setting-label" style="max-width: 50%;">
- <p class="mb-0">Portfolio Source</p>
- <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>
- </div>
- <div class="ml-3">
- <b-form-select v-model="settings.profile_source" :options="profileSourceOptions" :disabled="!user.statuses_count" />
- </div>
- </div>
- </div>
- <div v-else-if="tabIndex === 'Curate'" class="col-12 col-md-8 mt-3 py-2 px-0" key="1">
- <div v-if="!recentPostsLoaded" class="d-flex align-items-center justify-content-center py-5 my-5">
- <div class="text-center">
- <div class="spinner-border" role="status">
- <span class="sr-only">Loading...</span>
- </div>
- <p class="text-muted">Loading recent posts...</p>
- </div>
- </div>
- <template v-else>
- <div class="mt-n2 mb-4">
- <p class="text-muted small">Select up to 100 photos from your 100 most recent posts. You can only select public photo posts, videos are not supported at this time.</p>
- <div class="d-flex align-items-center justify-content-between">
- <p class="font-weight-bold mb-0">Selected {{ selectedRecentPosts.length }}/100</p>
- <div>
- <button
- class="btn btn-link font-weight-bold mr-3 text-decoration-none"
- :disabled="!selectedRecentPosts.length"
- @click="clearSelected">
- Clear selected
- </button>
- <button
- class="btn btn-primary py-0 font-weight-bold"
- style="width: 150px;"
- :disabled="!canSaveCurated"
- @click="saveCurated()">
- <template v-if="!isSavingCurated">Save</template>
- <b-spinner v-else small />
- </button>
- </div>
- </div>
- </div>
- <div class="d-flex justify-content-between align-items-center">
- <span @click="recentPostsPrev">
- <i :class="prevClass" />
- </span>
- <div class="row flex-grow-1 mx-2">
- <div v-for="(post, index) in recentPosts.slice(rpStart, rpStart + 9)" class="col-12 col-md-4 mb-1 p-1">
- <div class="square user-select-none" @click.prevent="toggleRecentPost(post.id)">
- <transition name="fade">
- <img
- :key="post.id"
- :src="getPreviewUrl(post)"
- width="100%"
- height="300"
- style="overflow: hidden;object-fit: cover;"
- :draggable="false"
- loading="lazy"
- onerror="this.src='/storage/no-preview.png';this.onerror=null;"
- class="square-content pr-1">
- </transition>
- <div v-if="selectedRecentPosts.indexOf(post.id) !== -1" style="position: absolute;right: -5px;bottom:-5px;">
- <div class="selected-badge">{{ selectedRecentPosts.indexOf(post.id) + 1 }}</div>
- </div>
- </div>
- </div>
- </div>
- <span @click="recentPostsNext()">
- <i :class="nextClass" />
- </span>
- </div>
- </template>
- </div>
- <div v-else-if="tabIndex === 'Customize'" class="col-12 mt-3 py-2" key="2">
- <div class="row">
- <div class="col-12 col-md-6">
- <div v-for="setting in customizeSettings" class="card bg-dark mb-5">
- <div class="card-header">{{ setting.title }}</div>
- <div class="list-group bg-dark">
- <div v-for="item in setting.items" class="list-group-item">
- <div class="d-flex justify-content-between align-items-center py-2">
- <div class="setting-label">
- <p class="mb-0">{{ item.label }}</p>
- <p v-if="item.description" class="small text-muted mb-0">{{ item.description }}</p>
- </div>
- <div class="setting-switch mt-n1">
- <b-form-checkbox
- v-model="settings[item.model]"
- name="check-button"
- size="lg"
- switch
- :disabled="item.requiredWithTrue && !settings[item.requiredWithTrue]" />
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div class="col-12 col-md-6">
- <div class="card bg-dark mb-5">
- <div class="card-header">Portfolio</div>
- <div class="list-group bg-dark">
- <div class="list-group-item">
- <div class="d-flex justify-content-between align-items-center py-2">
- <div class="setting-label">
- <p class="mb-0">Layout</p>
- </div>
- <div>
- <b-form-select v-model="settings.profile_layout" :options="profileLayoutOptions" />
- </div>
- </div>
- </div>
- <div v-if="settings.profile_source === 'custom'" class="list-group-item">
- <div class="d-flex justify-content-between align-items-center py-2">
- <div class="setting-label">
- <p class="mb-0">Order</p>
- </div>
- <div>
- <b-form-select
- v-model="settings.feed_order"
- :options="profileLayoutFeedOrder" />
- </div>
- </div>
- </div>
- <div class="list-group-item">
- <div class="d-flex justify-content-between align-items-center py-2">
- <div class="setting-label">
- <p class="mb-0">Color Scheme</p>
- </div>
- <div>
- <b-form-select
- v-model="settings.color_scheme"
- :options="profileLayoutColorSchemeOptions"
- :disabled="settings.color_scheme === 'custom'"
- @change="updateColorScheme" />
- </div>
- </div>
- </div>
- <div class="list-group-item">
- <div class="d-flex justify-content-between align-items-center py-2">
- <div class="setting-label">
- <p class="mb-0">Background Color</p>
- </div>
- <b-col sm="2">
- <b-form-input
- v-model="settings.background_color"
- debounce="1000"
- type="color"
- @change="updateBackgroundColor" />
- <b-button
- v-if="!['#000000', null].includes(settings.background_color)"
- variant="link"
- @click="resetBackgroundColor">
- Reset
- </b-button>
- </b-col>
- </div>
- </div>
- <div class="list-group-item">
- <div class="d-flex justify-content-between align-items-center py-2">
- <div class="setting-label">
- <p class="mb-0">Text Color</p>
- </div>
- <b-col sm="2">
- <b-form-input
- v-model="settings.text_color"
- debounce="1000"
- type="color"
- @change="updateTextColor" />
- <b-button
- v-if="!['#d4d4d8', null].includes(settings.text_color)"
- variant="link"
- @click="resetTextColor">
- Reset
- </b-button>
- </b-col>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
- <div v-else-if="tabIndex === 'Share'" class="col-12 col-md-8 bg-dark mt-3 py-2 rounded" key="3">
- <div class="py-2">
- <p class="text-muted">Portfolio URL</p>
- <p class="lead mb-0"><a :href="settings.url">{{ settings.url }}</a></p>
- </div>
- </div>
- </transition>
- </div>
- </div>
- </template>
- <script type="text/javascript">
- export default {
- data() {
- return {
- loading: true,
- tabIndex: "Configure",
- tabs: [
- "Configure",
- "Customize",
- "View Portfolio"
- ],
- user: undefined,
- settings: undefined,
- recentPostsLoaded: false,
- rpStart: 0,
- recentPosts: [],
- recentPostsPage: undefined,
- selectedRecentPosts: [],
- isSavingCurated: false,
- canSaveCurated: false,
- customizeSettings: [],
- skipWatch: false,
- profileSourceOptions: [
- { value: null, text: 'Please select an option', disabled: true },
- { value: 'recent', text: 'Most recent posts' },
- ],
- profileLayoutOptions: [
- { value: null, text: 'Please select an option', disabled: true },
- { value: 'grid', text: 'Grid' },
- { value: 'masonry', text: 'Masonry' },
- { value: 'album', text: 'Album' },
- ],
- profileLayoutColorSchemeOptions: [
- { value: null, text: 'Please select an option', disabled: true },
- { value: 'light', text: 'Light mode' },
- { value: 'dark', text: 'Dark mode' },
- { value: 'custom', text: 'Custom color scheme', disabled: true },
- ],
- profileLayoutFeedOrder: [
- { value: 'oldest', text: 'Oldest first' },
- { value: 'recent', text: 'Recent first' }
- ]
- }
- },
- computed: {
- prevClass() {
- return this.rpStart === 0 ?
- "fa fa-arrow-circle-left fa-3x text-dark" :
- "fa fa-arrow-circle-left fa-3x text-muted cursor-pointer";
- },
- nextClass() {
- return this.rpStart > (this.recentPosts.length - 9) ?
- "fa fa-arrow-circle-right fa-3x text-dark" :
- "fa fa-arrow-circle-right fa-3x text-muted cursor-pointer";
- },
- },
- watch: {
- settings: {
- deep: true,
- immediate: true,
- handler: function(o, n) {
- if(this.loading || this.skipWatch) {
- return;
- }
- if(!n.show_timestamp) {
- this.settings.show_link = false;
- }
- this.updateSettings();
- }
- }
- },
- mounted() {
- this.fetchUser();
- },
- methods: {
- fetchUser() {
- axios.get('/api/v1/accounts/verify_credentials')
- .then(res => {
- this.user = res.data;
- if(res.data.statuses_count > 0) {
- this.profileSourceOptions = [
- { value: null, text: 'Please select an option', disabled: true },
- { value: 'recent', text: 'Most recent posts' },
- { value: 'custom', text: 'Curated posts' },
- ];
- } else {
- setTimeout(() => {
- this.settings.active = false;
- this.settings.profile_source = 'recent';
- this.tabIndex = 'Configure';
- }, 1000);
- }
- })
- axios.post(this.apiPath('/api/portfolio/self/settings.json'))
- .then(res => {
- this.settings = res.data;
- this.updateTabs();
- if(res.data.metadata && res.data.metadata.posts) {
- this.selectedRecentPosts = res.data.metadata.posts;
- }
- if(res.data.color_scheme != 'dark') {
- if(res.data.color_scheme === 'light') {
- this.updateBackgroundColor('#ffffff');
- } else {
- if(res.data.hasOwnProperty('background_color')) {
- this.updateBackgroundColor(res.data.background_color);
- }
- if(res.data.hasOwnProperty('text_color')) {
- this.updateTextColor(res.data.text_color);
- }
- }
- }
- })
- .then(() => {
- this.initCustomizeSettings();
- })
- .then(() => {
- const url = new URL(window.location);
- if(url.searchParams.has('tab')) {
- let tab = url.searchParams.get('tab');
- let tabs = this.settings.profile_source === 'custom' ?
- ['curate', 'customize', 'share'] :
- ['customize', 'share'];
- if(tabs.indexOf(tab) !== -1) {
- this.toggleTab(tab.slice(0, 1).toUpperCase() + tab.slice(1));
- }
- }
- })
- .then(() => {
- setTimeout(() => {
- this.loading = false;
- }, 500);
- })
- },
- apiPath(path) {
- return path;
- },
- toggleTab(idx) {
- if(idx === 'Curate' && !this.recentPostsLoaded) {
- this.loadRecentPosts();
- }
- this.tabIndex = idx;
- this.rpStart = 0;
- if(idx == 'Configure') {
- const url = new URL(window.location);
- url.searchParams.delete('tab');
- window.history.pushState({}, '', url);
- } else if (idx == 'View Portfolio') {
- this.tabIndex = 'Configure';
- window.location.href = `https://${window._portfolio.domain}${window._portfolio.path}/${this.user.username}`;
- return;
- } else {
- const url = new URL(window.location);
- url.searchParams.set('tab', idx.toLowerCase());
- window.history.pushState({}, '', url);
- }
- },
- updateTabs() {
- if(this.settings.profile_source === 'custom') {
- this.tabs = [
- "Configure",
- "Curate",
- "Customize",
- "View Portfolio"
- ];
- } else {
- this.tabs = [
- "Configure",
- "Customize",
- "View Portfolio"
- ];
- }
- },
- updateSettings(silent = false) {
- if(this.skipWatch) {
- return;
- }
- axios.post(this.apiPath('/api/portfolio/self/update-settings.json'), this.settings)
- .then(res => {
- this.updateTabs();
- if(!silent) {
- this.$bvToast.toast(`Your settings have been successfully updated!`, {
- variant: 'dark',
- title: 'Settings Updated',
- autoHideDelay: 2000,
- appendToast: false
- })
- }
- })
- },
- loadRecentPosts() {
- axios.get('/api/v1/accounts/' + this.user.id + '/statuses?only_media=1&media_types=photo&limit=100&_pe=1')
- .then(res => {
- if(res.data.length) {
- this.recentPosts = res.data.filter(p => ['photo', 'photo:album'].includes(p.pf_type) && p.visibility === "public");
- }
- })
- .then(() => {
- setTimeout(() => {
- this.recentPostsLoaded = true;
- }, 500);
- })
- },
- toggleRecentPost(id) {
- if(this.selectedRecentPosts.indexOf(id) == -1) {
- if(this.selectedRecentPosts.length === 100) {
- return;
- }
- this.selectedRecentPosts.push(id);
- } else {
- this.selectedRecentPosts = this.selectedRecentPosts.filter(i => i !== id);
- }
- this.canSaveCurated = true;
- },
- recentPostsPrev() {
- if(this.rpStart === 0) {
- return;
- }
- this.rpStart = this.rpStart - 9;
- },
- recentPostsNext() {
- if(this.rpStart > (this.recentPosts.length - 9)) {
- return;
- }
- this.rpStart = this.rpStart + 9;
- },
- clearSelected() {
- this.selectedRecentPosts = [];
- },
- saveCurated() {
- this.isSavingCurated = true;
- event.currentTarget?.blur();
- axios.post('/api/portfolio/self/curated.json', {
- ids: this.selectedRecentPosts
- })
- .then(res => {
- this.isSavingCurated = false;
- this.$bvToast.toast(`Your curated posts have been updated!`, {
- variant: 'dark',
- title: 'Portfolio Updated',
- autoHideDelay: 2000,
- appendToast: false
- })
- })
- .catch(err => {
- this.isSavingCurated = false;
- this.$bvToast.toast(`An error occured while attempting to update your portfolio, please try again later and contact an admin if this problem persists.`, {
- variant: 'dark',
- title: 'Error',
- autoHideDelay: 2000,
- appendToast: false
- })
- })
- },
- initCustomizeSettings() {
- this.customizeSettings = [
- {
- title: "Post Settings",
- items: [
- {
- label: "Show Captions",
- model: "show_captions"
- },
- {
- label: "Show License",
- model: "show_license"
- },
- {
- label: "Show Location",
- model: "show_location"
- },
- {
- label: "Show Timestamp",
- model: "show_timestamp"
- },
- {
- label: "Link to Post",
- description: "Add link to timestamp to view the original post url, requires show timestamp to be enabled",
- model: "show_link",
- requiredWithTrue: "show_timestamp"
- }
- ]
- },
- {
- title: "Profile Settings",
- items: [
- {
- label: "Show Avatar",
- model: "show_avatar"
- },
- {
- label: "Show Bio",
- model: "show_bio"
- },
- {
- label: "Show View Profile Button",
- model: "show_profile_button"
- },
- {
- label: "Enable RSS Feed",
- description: "Enable your RSS feed with the 10 most recent portfolio items",
- model: "rss_enabled"
- },
- {
- label: "Show RSS Feed Button",
- model: "show_rss_button",
- requiredWithTrue: "rss_enabled"
- },
- ]
- },
- ]
- },
- updateBackgroundColor(e) {
- this.skipWatch = true;
- let rs = document.querySelector(':root');
- rs.style.setProperty('--body-bg', e);
- if(e !== '#000000' && e !== '#ffffff') {
- this.settings.color_scheme = 'custom';
- }
- this.$nextTick(() => {
- this.skipWatch = false;
- });
- },
- updateTextColor(e) {
- this.skipWatch = true;
- let rs = document.querySelector(':root');
- rs.style.setProperty('--text-color', e);
- if(e !== '#d4d4d8') {
- this.settings.color_scheme = 'custom';
- }
- this.$nextTick(() => {
- this.skipWatch = false;
- });
- },
- resetBackgroundColor() {
- this.skipWatch = true;
- this.$nextTick(() => {
- this.updateBackgroundColor('#000000');
- this.settings.color_scheme = 'dark';
- this.settings.background_color = '#000000';
- this.updateSettings(true);
- setTimeout(() => {
- this.skipWatch = false;
- }, 1000);
- });
- },
- resetTextColor() {
- this.skipWatch = true;
- this.$nextTick(() => {
- this.updateTextColor('#d4d4d8');
- this.settings.color_scheme = 'dark';
- this.settings.text_color = '#d4d4d8';
- this.updateSettings(true);
- setTimeout(() => {
- this.skipWatch = false;
- }, 1000);
- });
- },
- updateColorScheme(e) {
- if(e === 'light') {
- this.updateBackgroundColor('#ffffff');
- }
- if(e === 'dark') {
- this.updateBackgroundColor('#000000');
- }
- },
- getPreviewUrl(post) {
- let media = post.media_attachments[0];
- if(!media) { return '/storage/no-preview.png'; }
- if(media.preview_url && !media.preview_url.endsWith('/no-preview.png')) {
- return media.preview_url;
- }
- return media.url;
- }
- }
- }
- </script>
|