I'm new to vue and Nuxt and I have a task to deploy a static page with youtube api and search feature
So Everytime I try to generate a static page using nuxt it comes without my pics or youtube api
index.vue
<template>
<div class="iframe">
<input class="textR" type="text" v-model="searchInput">
<button class="button" style="vertical-align:middle" #click="search(searchInput)" ><span> Search</span></button>
<div v-if="videos!=null" id="iframeContainer" style="display: inline-table;">
<iframe class="overall" v-for="item in videos" :key="item.id.videoId" width="640" height="480"
:src="'https://www.youtube.com/embed/'+item.id.videoId"
frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
allowfullscreen></iframe>
</div>
</div>
</template>
<script>
export default {
created() {
let axios = require('axios');
let params = {
part: 'snippet',
key: 'myApiCode',
q: 'كايروكى',
maxResults: 5,
type: 'video'
};
var parent = this;
axios.get('https://www.googleapis.com/youtube/v3/search', { params })
.then(response => {
parent.videos = response.data.items }
)
.catch(error => console.error(error));
},
mounted(){
setTimeout(function(){ let count = document.getElementById("iframeContainer").children;
if (count.length%2 != 0) {count[count.length -1].style.cssText = "margin: auto; display: table-cell;"};
},1000)
},
data() {
return {
videos: null,
loading: true,
searchInput: ""
}
},
methods: {
search(keyword){
let axios = require('axios');
let params = {
part: 'snippet',
key: 'AIzaSyBqJ9T8tven3Ge0_hNDWKjJ3lqOoL5N90s',
q: keyword,
maxResults: 5,
type: 'video'
};
var parent = this;
axios.get('https://www.googleapis.com/youtube/v3/search', { params })
.then(response => {
parent.videos = response.data.items }
)
.catch(error => console.error(error));
},
}
}
</script>
<style>
body {background-color: black;}
input[type=text] {
background-color: white;
background-image: url('../components/searchicon.png');
background-position: -1px 0px;
background-repeat: no-repeat;
padding-left: 30px;
background-size: 14%;
color : rgb(0, 0, 0);
}
.overall{
padding: 23px 20px;
background-color: #272727;
margin: 40px 50px;
border: 1px red solid;
}
.iframe {
padding: 23px 20px;
margin: 40px 100px;
}
.button {
display: inline-block;
border-radius: 4px;
background-color: #f4511e;
border: none;
color: #FFFFFF;
text-align: center;
font-size: 28px;
padding: 20px;
width: 200px;
transition: all 0.5s;
cursor: pointer;
margin: 5px;
}
.textR{
padding: 5px 5px;
margin: 20px 50px;
}
.button span {
cursor: pointer;
display: inline-block;
position: relative;
transition: 0.5s;
}
.button span:after {
content: '\00bb';
position: absolute;
opacity: 0;
top: 0;
right: -20px;
transition: 0.5s;
}
.button:hover span {
padding-right: 25px;
}
.button:hover span:after {
opacity: 1;
right: 0;
}
</style>
nuxt.config.js
import colors from 'vuetify/es5/util/colors'
// const routerBase = process.env.DEPLOY_ENV === 'master' ? {
// router: {
// base: '/youtube-task/'
// }
// } : {}
export default {
// ...routerBase,
mode: 'universal',
/*
** Headers of the page
*/
head: {
titleTemplate: '%s - ' + process.env.npm_package_name,
title: process.env.npm_package_name || '',
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'description', name: 'description', content: process.env.npm_package_description || '' }
],
link: [
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' }
]
},
// router: {
// base: '/youtube-task/'
// },
/*
** Customize the progress-bar color
*/
loading: { color: '#fff' },
/*
** Global CSS
*/
css: [
],
/*
** Plugins to load before mounting the App
*/
plugins: [
],
/*
** Nuxt.js dev-modules
*/
buildModules: [
'#nuxtjs/vuetify',
],
/*
** Nuxt.js modules
*/
modules: [
// Doc: https://axios.nuxtjs.org/usage
'#nuxtjs/axios',
],
/*
** Axios module configuration
** See https://axios.nuxtjs.org/options
*/
axios: {
},
/*
** vuetify module configuration
** https://github.com/nuxt-community/vuetify-module
*/
vuetify: {
customVariables: ['~/assets/variables.scss'],
theme: {
dark: true,
themes: {
dark: {
primary: colors.blue.darken2,
accent: colors.grey.darken3,
secondary: colors.amber.darken3,
info: colors.teal.lighten1,
warning: colors.amber.base,
error: colors.deepOrange.accent4,
success: colors.green.accent3
}
}
}
},
/*
** Build configuration
*/
build: {
/*
** You can extend webpack config here
*/
extend (config, ctx) {
}
}
}
package.json
{
"name": "youtube-task",
"version": "1.0.0",
"description": "My flawless Nuxt.js project",
"author": "MostafaDesoky",
"private": true,
"scripts": {
"dev": "nuxt",
"build": "nuxt build",
"start": "nuxt start",
"generate": "nuxt generate",
"build:gh-pages": "cross-env DEPLOY_ENV=master nuxt build",
"generate:gh-pages": "cross-env DEPLOY_ENV=master nuxt generate",
"deploy": "push-dir --dir=dist --branch=master --cleanup"
},
"dependencies": {
"nuxt": "^2.0.0",
"#nuxtjs/axios": "^5.3.6"
},
"devDependencies": {
"#nuxtjs/vuetify": "^1.0.0",
"cross-env": "^7.0.2",
"push-dir": "^0.4.1"
}
}
Anyhelp would be great I tried alot of methods but nothing
I tried to make netlify generate the dist for me and it was the same with no pic nor youtube api
Edit #1 : When I run it on my localhost it works perfectly through npm run dev,
the problem when I try to generate the dist folder and upload it to anywhere
Have you tried to enable spa mode before generating static website ?
mode: 'spa',
And why don't you use the axios helper directly instead of requiring it ?
this.$axios.get('https://www.googleapis.com/youtube/v3/search', { params })
For your background images you have to use this syntax :
background-image: url('~folder/nameoffile.ext');
Related
I get errors in the console about the lack of the required props (height / width). How can I find out where to specify them (in what file and what should the class be called)?
I am aware that could be a dummy question so I also would be grateful if you would reccomend some viedo/tutorial in your opinion useful to get to know more devtools basics.
This is my first question, so I'm not sure what information I should provide. Let me know if anything is missing.
My console 1
Home.vue
<template>
<div id="home">
<LazyHydrate when-idle>
<SfHero class="hero">
<SfHeroItem
v-for="(hero, i) in heroes"
:key="i"
:title="$t(hero.title)"
:subtitle="$t(hero.subtitle)"
:background="hero.background"
:image="hero.image | addBasePathFilter"
:class="hero.className"
:height="200"
:width="200"
/>
</SfHero>
</LazyHydrate>
<LazyHydrate when-visible>
<SfBannerGrid :banner-grid="1" class="banner-grid">
<template v-for="item in banners" v-slot:[item.slot]>
<SfBanner
:key="item.slot"
:title="$t(item.title)"
:subtitle="$t(item.subtitle)"
:description="$t(item.description)"
:button-text="$t(item.buttonText)"
:link="localePath(item.link)"
:image="item.image | addBasePathFilter"
:class="item.class"
:height="200"
:width="200"
/>
</template>
</SfBannerGrid>
</LazyHydrate>
<LazyHydrate when-visible>
<div class="similar-products">
<SfHeading :title="$t('Match with it')" :level="2" />
<nuxt-link :to="localePath('/c/women')" class="smartphone-only">See all</nuxt-link>
</div>
</LazyHydrate>
<LazyHydrate when-visible>
<SfCarousel
class="carousel"
:settings="{ peek: 16, breakpoints: { 1023: { peek: 0, perView: 2 } } }"
>
<template #prev="{go}">
<SfArrow
aria-label="prev"
class="sf-arrow--left sf-arrow--long"
#click="go('prev')"
/>
</template>
<template #next="{go}">
<SfArrow
aria-label="next"
class="sf-arrow--right sf-arrow--long"
#click="go('next')"
/>
</template>
<SfCarouselItem
class="carousel__item"
v-for="(product, i) in products"
:key="i"
>
<SfProductCard
class="carousel__item__product"
:title="product._name"
:image="productGetters.getCoverImage(product) | addBasePathFilter"
image-tag="nuxt-img"
:nuxt-img-config="{
format: 'webp',
fit: 'fill'
}"
:image-width="216"
:image-height="290"
:regular-price="productPriceTransform(product).regular"
:special-price="productPriceTransform(product).special"
:is-added-to-cart="isInCart({ product })"
:is-in-wishlist="isInWishlist({ product })"
show-add-to-cart-button
:link="localePath(`/p/${productGetters.getSlug(product)}/${productGetters.getSku(product)}`)"
#click:add-to-cart="addToCart({ product, quantity: 1 })"
#click:wishlist="!isInWishlist({ product }) ? addProductToWishlist(product) : removeProductFromWishlist(product)"
/>
</SfCarouselItem>
</SfCarousel>
</LazyHydrate>
<LazyHydrate when-visible>
<SfCallToAction
:title="$t('Subscribe to Newsletters')"
:button-text="$t('Subscribe')"
:description="$t('Be aware of upcoming sales and events. Receive gifts and special offers!')"
:image="'/homepage/newsletter.webp' | addBasePathFilter"
class="call-to-action"
>
<template #button>
<SfButton
class="sf-call-to-action__button"
data-testid="cta-button"
#click="handleNewsletterClick"
>
{{ $t('Subscribe') }}
</SfButton>
</template>
</SfCallToAction>
</LazyHydrate>
<LazyHydrate when-visible>
<NewsletterModal #email-submitted="onSubscribe" />
</LazyHydrate>
</div>
</template>
<script>
import {
SfHero,
SfBanner,
SfCallToAction,
SfCarousel,
SfProductCard,
SfBannerGrid,
SfHeading,
SfArrow,
SfButton
} from '#storefront-ui/vue';
import LazyHydrate from 'vue-lazy-hydration';
import { ref, computed, watch, useContext } from '#nuxtjs/composition-api';
import { onSSR } from '#vue-storefront/core';
import {
useCart,
useFacet,
useWishlist,
useCurrency,
facetGetters,
productGetters,
wishlistGetters,
productPriceTransform
} from '#vsf-enterprise/commercetools';
import NewsletterModal from '~/components/NewsletterModal.vue';
import { useUiState, useUiNotification } from '../composables';
export default {
name: 'Home',
setup() {
const { app: { i18n } } = useContext();
const { toggleNewsletterModal } = useUiState();
const { send } = useUiNotification();
const {
isInCart,
addItem: addItemToCart,
error: cartError
} = useCart();
const { result, search } = useFacet('home');
const { currency } = useCurrency();
const { addItem: addItemToWishlist, isInWishlist, removeItem: removeItemFromWishlist, wishlist, error: wishlistError } = useWishlist();
const products = computed(() => facetGetters.getProducts(result.value));
const fetchProducts = async () => {
await search({
filters: {},
page: 1,
itemsPerPage: 12,
sort: 'latest',
phrase: ''
});
};
watch(currency, async () => {
await fetchProducts();
});
onSSR(async () => {
await fetchProducts();
});
const mocks = {
heroes: [
{
title: 'Colorful summer dresses are already in store',
subtitle: 'SUMMER COLLECTION 2022',
background: '#eceff1',
image: '/homepage/bannerH.webp'
},
{
title: 'Colorful summer dresses are already in store',
subtitle: 'SUMMER COLLECTION 2022',
background: '#efebe9',
image: '/homepage/bannerA.webp',
className:
'sf-hero-item--position-bg-top-left sf-hero-item--align-right'
},
{
title: 'Colorful summer dresses are already in store',
subtitle: 'SUMMER COLLECTION 2022',
background: '#fce4ec',
image: '/homepage/bannerB.webp'
}
],
banners: [
{
slot: 'banner-A',
subtitle: 'Dresses',
title: 'Cocktail & Party',
description: 'Find stunning women\'s cocktail dresses and party dresses. Stand out in lace and metallic cocktail dresses from all your favorite brands.',
buttonText: 'Shop now',
image: '/homepage/bannerF.webp',
class: 'sf-banner--slim desktop-only',
link: '/c/women/women-clothing-skirts'
},
{
slot: 'banner-B',
subtitle: 'Dresses',
title: 'Linen Dresses',
description: 'Find stunning women\'s cocktail dresses and party dresses. Stand out in lace and metallic cocktail dresses from all your favorite brands.',
buttonText: 'Shop now',
image: '/homepage/bannerE.webp',
class: 'sf-banner--slim banner-central desktop-only',
link: '/c/women/women-clothing-dresses'
},
{
slot: 'banner-C',
subtitle: 'T-Shirts',
title: 'The Office Life',
image: '/homepage/bannerC.webp',
class: 'sf-banner--slim banner__tshirt',
link: '/c/women/women-clothing-shirts'
},
{
slot: 'banner-D',
subtitle: 'Summer Sandals',
title: 'Eco Sandals',
image: '/homepage/bannerG.webp',
class: 'sf-banner--slim',
link: '/c/women/women-shoes-sandals'
}
]
};
const heroes = ref(mocks.heroes);
const banners = ref(mocks.banners);
const handleNewsletterClick = () => {
toggleNewsletterModal();
};
const onSubscribe = (emailAddress) => {
console.log(`Email ${emailAddress} was added to newsletter.`);
toggleNewsletterModal();
};
const addToCart = async ({ product, quantity }) => {
const { id, sku } = product;
await addItemToCart({
product: { id, sku },
quantity
});
if (!cartError.value.addItem) {
send({
type: 'success',
message: i18n.t('Product has been added to the cart.')
});
}
};
const addProductToWishlist = async (product) => {
await addItemToWishlist({ product });
if (!wishlistError.value.addItem) {
send({
type: 'success',
message: i18n.t('Product has been added to the wishlist.')
});
}
};
const removeProductFromWishlist = async (productItem) => {
const productsInWhishlist = computed(() => wishlistGetters.getItems(wishlist.value));
const product = productsInWhishlist.value.find(wishlistProduct => wishlistProduct.variant.sku === productItem.sku);
await removeItemFromWishlist({ product });
if (!wishlistError.value.removeItem) {
send({
type: 'success',
message: i18n.t('Product has been removed from the wishlist.')
});
}
};
return {
heroes,
banners,
products,
productGetters,
handleNewsletterClick,
onSubscribe,
isInCart,
addToCart,
addProductToWishlist,
isInWishlist,
removeProductFromWishlist,
productPriceTransform
};
},
components: {
LazyHydrate,
NewsletterModal,
SfArrow,
SfBanner,
SfBannerGrid,
SfButton,
SfCallToAction,
SfCarousel,
SfHeading,
SfHero,
SfProductCard
},
beforeRouteEnter (_, _2, next) { next('/home-page') }
};
</script>
<style lang="scss">
.carousel__item__product {
.sf-product-card__title {
margin: var(--spacer-base) 0 var(--spacer-xs) 0;
}
.sf-product-card__add-button {
margin-bottom: var(--spacer-xl);
}
}
</style>
<style lang="scss" scoped>
#home {
box-sizing: border-box;
padding: 0 var(--spacer-sm);
#include for-desktop {
max-width: 1240px;
padding: 0;
margin: 0 auto;
}
}
.hero {
margin: var(--spacer-xl) auto var(--spacer-lg);
--hero-item-background-position: center;
#include for-desktop {
margin: var(--spacer-xl) auto var(--spacer-2xl);
}
.sf-hero-item {
min-height: 230px;
&:nth-child(even) {
--hero-item-background-position: left;
#include for-mobile {
--hero-item-background-position: 30%;
::v-deep .sf-hero-item__subtitle,
::v-deep .sf-hero-item__title {
text-align: right;
width: 100%;
padding-left: var(--spacer-sm);
}
}
}
}
::v-deep .sf-hero__control {
&--right,
&--left {
display: none;
}
}
}
.banner-grid {
--banner-container-width: 50%;
margin: var(--spacer-xl) 0;
::v-deep .sf-link:hover {
color: var(--c-white);
}
#include for-desktop {
margin: var(--spacer-2xl) 0;
::v-deep .sf-link {
--button-width: auto;
text-decoration: none;
}
}
}
.banner {
&__tshirt {
background-position: left;
}
&-central {
#include for-desktop {
--banner-container-flex: 0 0 70%;
}
}
}
.similar-products {
display: flex;
justify-content: space-between;
align-items: center;
padding-bottom: var(--spacer-2xs);
--heading-padding: 0;
border-bottom: 1px var(--c-light) solid;
#include for-desktop {
border-bottom: 0;
justify-content: center;
padding-bottom: 0;
}
}
.call-to-action {
background-position: right;
margin: var(--spacer-xs) 0;
#include for-desktop {
margin: var(--spacer-xl) 0 var(--spacer-2xl) 0;
}
}
.carousel {
margin: 0 calc(0 - var(--spacer-sm)) 0 0;
#include for-desktop {
margin: 0;
}
&__item {
margin: 1.375rem 0 2.5rem 0;
#include for-desktop {
margin: var(--spacer-xl) 0 var(--spacer-xl) 0;
}
&__product {
--product-card-add-button-transform: translate3d(0, 30%, 0);
::v-deep .sf-product-card {
&__title {
margin: var(--spacer-base) 0 var(--spacer-xs) 0;
}
&__add-button {
margin-bottom: var(--spacer-xl);
}
}
}
}
::v-deep .sf-arrow--long .sf-arrow--right {
--arrow-icon-transform: rotate(180deg);
-webkit-transform-origin: center;
transform-origin: center;
}
}
</style>
default.vue
<template>
<div>
<RenderContent v-if="styleGuide.length" :content="styleGuide" />
<LazyHydrate when-visible>
<TopBar class="desktop-only" />
</LazyHydrate>
<LazyHydrate when-idle>
<AppHeader />
</LazyHydrate>
<div id="layout">
<nuxt :key="$route.fullPath" />
<LazyHydrate when-visible>
<BottomNavigation />
</LazyHydrate>
<CartSidebar />
<WishlistSidebar />
<FiltersSidebar />
<LoginModal />
<Notification />
</div>
<LazyHydrate when-visible>
<AppFooter />
</LazyHydrate>
</div>
</template>
<script>
import AppHeader from '~/components/AppHeader.vue'
import BottomNavigation from '~/components/BottomNavigation.vue'
import AppFooter from '~/components/AppFooter.vue'
import TopBar from '~/components/TopBar.vue'
import CartSidebar from '~/components/CartSidebar.vue'
import WishlistSidebar from '~/components/WishlistSidebar.vue'
import FiltersSidebar from '~/components/FiltersSidebar.vue'
import LoginModal from '~/components/LoginModal.vue'
import Notification from '~/components/Notification'
import useCmsLayout from '~/composables/useCmsLayout'
import { onMounted } from '#vue/composition-api'
import LazyHydrate from 'vue-lazy-hydration'
import { useStore, useUser, useWishlist } from '#vsf-enterprise/commercetools'
import { onSSR } from '#vue-storefront/core'
export default {
name: 'DefaultLayout',
components: {
LazyHydrate,
TopBar,
AppHeader,
BottomNavigation,
AppFooter,
CartSidebar,
WishlistSidebar,
FiltersSidebar,
LoginModal,
Notification,
},
setup() {
const { load: loadStores } = useStore()
const { load: loadUser } = useUser()
const { load: loadWishlist } = useWishlist()
const { getLayout, styleGuide } = useCmsLayout()
onSSR(async () => {
await Promise.all([loadStores(), getLayout()])
})
onMounted(async () => {
await Promise.all([loadUser(), loadWishlist()])
})
return {
styleGuide,
}
},
head() {
return this.$nuxtI18nHead({ addSeoAttributes: true })
},
}
</script>
<style lang="scss">
#import '~#storefront-ui/vue/styles';
#layout {
box-sizing: border-box;
#include for-desktop {
max-width: 1240px;
margin: auto;
}
}
.no-scroll {
overflow: hidden;
height: 100vh;
}
// Reset CSS
html {
width: auto;
#include for-mobile {
overflow-x: hidden;
}
}
body {
overflow-x: hidden;
color: var(--c-text);
font-size: var(--font-size--base);
font-family: var(--font-family--primary);
margin: 0;
padding: 0;
}
a {
text-decoration: none;
color: var(--c-link);
&:hover {
color: var(--c-link-hover);
}
}
h1 {
font-family: var(--font-family--secondary);
font-size: var(--h1-font-size);
line-height: 1.6;
margin: 0;
}
h2 {
font-family: var(--font-family--secondary);
font-size: var(--h2-font-size);
line-height: 1.6;
margin: 0;
}
h3 {
font-family: var(--font-family--secondary);
font-size: var(--h3-font-size);
line-height: 1.6;
margin: 0;
}
h4 {
font-family: var(--font-family--secondary);
font-size: var(--h4-font-size);
line-height: 1.6;
margin: 0;
}
</style>
Welcome on stackoverflow!
This error is triggered when a Vue component declares "props" as required, and they are not provided.
The logs says:
Missing required prop height. ------------------- SfImage.vue
So this error is trigger from your SfImage.vue component.
Hence I guess you declared the height and width props from this component as required.
Solution:
You remove the required: true option of your props
OR
You correctly provide these props when you use that component: <SfImage height="40px" width="40px" />
The error in the Chrome Dev Tools usually tells you the name of the component you used without providing the required props. In a vue component, props are declared inside it, under the props property (if using vue2), or under declareProps macro (if using vue3)
For more info, you can read about vue component props in the official documentation: https://vuejs.org/guide/components/props.html
I have problem with setting rotation axis in a animation of menu buttons in react webpage.
Im using react 17.0.2 and react-transition-group 4.4.2.
The concept of my menu is to rotate the button after it has been clicked.
If other button was clicked before, it shoud rotate back to its original position, and the new button should rotate - to show it's active.
I have the problem with making animation axis of rotation perfectlly centered:
[Menu buttons before rotation] [1]
: https://i.stack.imgur.com/7nVnv.jpg
[After rotation menu button is moved to the right] [2]
: https://i.stack.imgur.com/yB28J.jpg
As you can see in the image [2] middle menu button moved to the right and is not aligned with the rest of the buttons, I want it to be perfectlly aligned with all the buttons.
My NavigationItems.js describes the buttons:
import React, { useState } from "react";
import { TransitionGroup } from "react-transition-group";
import classes from "./NavigationItems.module.scss";
import NavigationItem from "./NavigationItem/NavigationItem"
const data = [
{in:false, id:0, desc:"About me",href:"/photo-gallery/info"},
{in:false, id:1, desc:"Photo gallery",href:"/photo-gallery/photos"},
{in:false, id:2, desc:"Some tests",href:"/photo-gallery/education"}
];
const NavigationItems = () => {
const [allButtons, setAllButtons] = useState(data);
const [prevButton, setPrevButton] = useState({
in:false, id:-1, desc:"",href:""
});
const allButtonsDeepUpdate = (idx, obj, updatePrevButton) => {
const allButtonsCpy = [];
for(let i=0;i<allButtons.length;i++) {
if(i===idx) {
allButtonsCpy.push(Object.assign({},obj));
} else if (updatePrevButton && i===prevButton.id) {
allButtonsCpy.push(Object.assign({},prevButton));
} else {
allButtonsCpy.push(Object.assign({},allButtons[i]));
};
};
setAllButtons(allButtonsCpy);
};
const enterAnimation = (idx) => {
if(allButtons[idx].id !== prevButton.id) {
const newButton = {...allButtons[idx], ...{in:true}};
if (prevButton.id !== -1)
setPrevButton({...prevButton,...{in:false}});
console.log("newButton:",newButton) ;
console.log("prevButton:",prevButton);
allButtonsDeepUpdate(idx, newButton, prevButton.id>=0 ? true : false)
setPrevButton(Object.assign({},allButtons[idx]));
}
};
return (
<div>
<TransitionGroup component="ul" className={classes.NavigationItems}>
{allButtons.map((button) => (
<NavigationItem
starter={button.in}
pkey={button.id}
timeout={1000}
click={enterAnimation.bind(this,button.id)}
link={button.href}
>
{button.desc}
</NavigationItem>
))}
</TransitionGroup>
</div>
);
};
export default NavigationItems;
The coresponding scss classes (NavigationItems.module.scss) of NavigationItems.js look like this:
#import '../../../sass/abstracts/variables.scss';
.NavigationItems {
margin: 0;
padding: 0;
list-style: none;
display: block;
//flex-flow: column;
//flex-direction: column;
//justify-content: center;
height: 100%;
}
#media (min-width: $min-width-small-res) {
.NavigationItems {
flex-flow: row;
flex-direction: row;
justify-content: space-evenly;
}
}
Note that when I switch styles to flexbox the rotation axis is also not centered properly.
The NavigationItem.js I am animating looks like this (I am using react-router-dom 6 but it can be also used with 5):
import React from 'react';
import { NavLink } from 'react-router-dom';
import { CSSTransition } from 'react-transition-group';
import classes from './NavigationItem.module.scss';
const NavigationItem = (props) => {
const nodeRef = React.createRef();
return (
<CSSTransition
key={props.pkey}
nodeRef={nodeRef}
in={props.starter}
classNames={{
enter: classes.NavigationItemEnter,
enterActive: classes.NavigationItemEnterActive,
enterDone: classes.NavigationItemEnterDone,
exit: classes.NavigationItemExit,
exitActive: classes.NavigationItemExitActive,
exitDone: classes.NavigationItemExitDone,
}}
timeout={props.timeout}
>
<li ref={nodeRef} className={classes.NavigationItem} onClick={props.click}>
<NavLink
// activeClassName={classes.active} // react-router-dom v.5
//className={({isActive}) => isActive ? classes.active : ''} // v.6
to={props.link}
exact={props.exact}
>
{props.children}
</NavLink>
</li>
</CSSTransition>
);
}
export default NavigationItem;
The coresponding classes (NavigationItem.module.scss) look like this:
#import '../../../../sass/abstracts/variables.scss';
.NavigationItem {
box-sizing: border-box;
display: inline-block;
width: 100%;
//backface-visibility: hidden;
transform-origin: center center 0;
&Enter {
transform: rotate(0deg);
}
&EnterActive {
transform: rotate(180deg);
transition: transform 500ms linear
}
&EnterDone {
transform: rotate(180deg);
}
&Exit {
transform: rotate(180deg);
}
&ExitActive {
transform: rotate(0deg);
transition: transform 500ms linear;
}
&ExitDone {
transform: rotate(0deg);
}
& a {
border-radius: 15%;
margin-right: 15px;
background-color: $color-secondary-light;
color: $color-primary;
text-decoration: none;
width: 100%;
//box-sizing: border-box;
display: block;
}
& a:hover {
color: $color-alert;
}
& a:active,
& a.active {
color: $color-quaduprary;
background-color: $color-secondary-dark;
}
}
#media (min-width: $min-width-small-res) {
.NavigationItem {
margin: 0;
left: 0;
display: flex;
height: 100%;
width: auto;
align-items: center;
& a {
color: $color-tertiary;
height: 100%;
padding: 10px 10px;
}
& a:active,
& a.active {
background-color: $color-secondary-dark;
color: $color-quaduprary;
border-color: $color-alert;
}
}
}
The last files are package.json and variables.scss:
$color-primary: #845EC2;
$color-secondary-light: #FF6F91;
$color-secondary-dark: #D65DB1;
$color-tertiary: #009b1a;
$color-quaduprary: #009b1a;
$color-alert: #FFC75F;
//sizes
$menu-button-width: 9.5rem;
//text
$text-large: 1.5rem;
$text-small: 1rem;
$text-supersmall: 0.8rem;
//round pixel
$round-small: 3px;
$round-medium: 10rem;
$round-large: 50%;
//CV header
$photoHeight: 80%;
$small-res-photoHeight: 90%;
//Media Queries
$max-width-intermediate-res: 1050px;
$max-width-medium-res: 730px;
$max-width-small-res: 564px;
$min-width-small-res: 565px;
//csv filters:
$filter-main: invert(93%) sepia(90%) saturate(2%) hue-rotate(357deg) brightness(108%) contrast(100%);
json:
{
"name": "photo-gallery",
"version": "0.1.0",
"private": true,
"dependencies": {
"#testing-library/jest-dom": "^5.16.2",
"#testing-library/react": "^12.1.4",
"#testing-library/user-event": "^13.5.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-router-dom": "^6.2.2",
"react-scripts": "5.0.0",
"react-transition-group": "^4.4.2",
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"sass": "^1.49.9"
}
}
As you can see I tried to center rotation axis with "transform-origin: center center 0;" (also tried "transform-origin: center center" and "transform-origin: center) but with no luck.
I think I'm missing something here, any help, suggestions would be appreciated!
I also have two warnings with js code above regarding keys in rendering list and deprecated findDOMNode in StrictMode but that is the material for 2 next posts.
Thanks in advance for your answers!!!
As it was mentioned in first comment, the value of an anchor was set to 15px which made it rotate out of center. In order to make it work I either have to remove margin-right, or add margin-left with the same value.
Thanks a lot A Howorth!
I'm trying to use a property of my data in a computed method like this:
data() {
return {
ToDoItems: [
{ id: uniqueId("todo-"), label: "Learn Vue", done: false },
{
id: uniqueId("todo-"),
label: "Create a Vue project with the CLI",
done: true,
},
{ id: uniqueId("todo-"), label: "Have fun", done: true },
{ id: uniqueId("todo-"), label: "Create a to-do list", done: false },
],
};
},
computed: {
listSummary() {
const numberFinishedItems = this.ToDoItems.filter((item) => item.done)
.length;
return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed`;
},
},
But the IDE (Visual Studio Code) and the compiler throw an error:
Property 'ToDoItems' does not exist on type 'ComponentPublicInstance<{}, {}, {}, {}, {}, EmitsOptions, {}, {}, false, ComponentOptionsBase<{}, {}, {}, {}, {}, ComponentOptionsMixin, ComponentOptionsMixin, EmitsOptions, string, {}>>'.
I'm following the vue.js tutorial of mozilla (https://developer.mozilla.org/en-US/docs/Learn/Tools_and_testing/Client-side_JavaScript_frameworks/Vue_computed_properties#adding_a_summary_counter) but using v3.
Has anything changed that this isn't possible anymore / differently?
Thanks in advance
complete code:
<template>
<div id="app">
<h1>To-Do List</h1>
<to-do-form #todo-added="addToDo"></to-do-form>
<h2 id="list-summary">{{ listSummary }}</h2>
<ul aria-labelledby="list-summary" class="stack-large">
<li v-for="item in ToDoItems" :key="item.id">
<to-do-item :label="item.label" :done="true" :id="item.id"></to-do-item>
</li>
</ul>
</div>
</template>
<script lang="ts">
import uniqueId from "lodash.uniqueid";
import { defineComponent } from "vue";
import ToDoItem from "./components/ToDoItem.vue";
import ToDoForm from "./components/ToDoForm.vue";
export default defineComponent({
name: "App",
components: {
ToDoItem,
ToDoForm,
},
data() {
return {
ToDoItems: [
{ id: uniqueId("todo-"), label: "Learn Vue", done: false },
{
id: uniqueId("todo-"),
label: "Create a Vue project with the CLI",
done: true,
},
{ id: uniqueId("todo-"), label: "Have fun", done: true },
{ id: uniqueId("todo-"), label: "Create a to-do list", done: false },
],
};
},
methods: {
addToDo(toDoLabel: string) {
this.ToDoItems.push({
id: uniqueId("todo-"),
label: toDoLabel,
done: false,
});
},
},
computed: {
listSummary() {
const numberFinishedItems = this.ToDoItems.filter((item) => item.done)
.length;
return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed`;
},
},
});
</script>
<style>
/* Global styles */
.btn {
padding: 0.8rem 1rem 0.7rem;
border: 0.2rem solid #4d4d4d;
cursor: pointer;
text-transform: capitalize;
}
.btn__danger {
color: #fff;
background-color: #ca3c3c;
border-color: #bd2130;
}
.btn__filter {
border-color: lightgrey;
}
.btn__danger:focus {
outline-color: #c82333;
}
.btn__primary {
color: #fff;
background-color: #000;
}
.btn-group {
display: flex;
justify-content: space-between;
}
.btn-group > * {
flex: 1 1 auto;
}
.btn-group > * + * {
margin-left: 0.8rem;
}
.label-wrapper {
margin: 0;
flex: 0 0 100%;
text-align: center;
}
[class*="__lg"] {
display: inline-block;
width: 100%;
font-size: 1.9rem;
}
[class*="__lg"]:not(:last-child) {
margin-bottom: 1rem;
}
#media screen and (min-width: 620px) {
[class*="__lg"] {
font-size: 2.4rem;
}
}
.visually-hidden {
position: absolute;
height: 1px;
width: 1px;
overflow: hidden;
clip: rect(1px 1px 1px 1px);
clip: rect(1px, 1px, 1px, 1px);
clip-path: rect(1px, 1px, 1px, 1px);
white-space: nowrap;
}
[class*="stack"] > * {
margin-top: 0;
margin-bottom: 0;
}
.stack-small > * + * {
margin-top: 1.25rem;
}
.stack-large > * + * {
margin-top: 2.5rem;
}
#media screen and (min-width: 550px) {
.stack-small > * + * {
margin-top: 1.4rem;
}
.stack-large > * + * {
margin-top: 2.8rem;
}
}
/* End global styles */
#app {
background: #fff;
margin: 2rem 0 4rem 0;
padding: 1rem;
padding-top: 0;
position: relative;
box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 2.5rem 5rem 0 rgba(0, 0, 0, 0.1);
}
#media screen and (min-width: 550px) {
#app {
padding: 4rem;
}
}
#app > * {
max-width: 50rem;
margin-left: auto;
margin-right: auto;
}
#app > form {
max-width: 100%;
}
#app h1 {
display: block;
min-width: 100%;
width: 100%;
text-align: center;
margin: 0;
margin-bottom: 1rem;
}
</style>
Boussadjra Brahim's answer is "alright" but it doesn't actually address the issue. user16362509 is correct, but doesn't answer the question.. What you need to do is yes, annotate the return types of both your data properties and computed properties so TS knows what's going on. Not only that, but it provides stricter type checking if you annotate all types. I don't believe this issue actually occurs when properly using Vue3's composition API, but the problem does, as you can see, exist in options Api. Try: (Or consider using Composition Api). Weirdly enough, when you annotate the return type of a computed method, it understands the context, just quirks of Vue but it's good practice to have the types explicit.
<script lang="ts">
import uniqueId from "lodash.uniqueid";
import { defineComponent } from "vue";
import ToDoItem from "./components/ToDoItem.vue";
import ToDoForm from "./components/ToDoForm.vue";
export default defineComponent({
name: "App",
components: {
ToDoItem,
ToDoForm,
},
data(): { ToDoItems: Array<{id: *string*, label: string, done: boolean}> } {
return {
ToDoItems: [
{ id: uniqueId("todo-"), label: "Learn Vue", done: false },
{
id: uniqueId("todo-"),
label: "Create a Vue project with the CLI",
done: true,
},
{ id: uniqueId("todo-"), label: "Have fun", done: true },
{ id: uniqueId("todo-"), label: "Create a to-do list", done: false },
],
};
},
methods: {
addToDo(toDoLabel: string): void {
this.ToDoItems.push({
id: uniqueId("todo-"),
label: toDoLabel,
done: false,
});
},
},
computed: {
listSummary(): string {
const numberFinishedItems = this.ToDoItems.filter((item) => item.done)
.length;
return `${numberFinishedItems} out of ${this.ToDoItems.length} items completed`;
},
},
});
</script>
You are already using Vue 3. Why not use composition API with script setup for even better typescript support?
Live demo
<template>
<div id="app">
<h1>To-Do List</h1>
<form #submit.prevent="addToDo">
<input type="text" ref="label" />
<button type="submit">Add</button>
</form>
<h2 id="list-summary">{{ listSummary }}</h2>
<ul aria-labelledby="list-summary" class="stack-large">
<li v-for="item in ToDoItems" :key="item.id">
<input type="checkbox" v-model="item.done" />
{{ item.id }} {{ item.label }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue';
interface ToDoItem {
id: string;
label: string;
done: boolean;
}
const label = ref(null)
const ToDoItems = ref<ToDoItem[]>([
{ id: 1, label: 'Learn Vue', done: false },
{ id: 2, label: 'Create a Vue project with the CLI', done: true },
{ id: 3, label: 'Have fun', done: true },
{ id: 4, label: 'Create a to-do list', done: false },
]);
const addToDo = () => {
ToDoItems.value.push({
id: ToDoItems.value.length + 1,
label: label.value.value,
done: false,
});
label.value.value = '';
};
const listSummary = computed(() => {
return `${ToDoItems.value.filter((item) => item.done).length} out of ${ToDoItems.value.length} items completed`;
});
</script>
I have angular 8 application and I can show a popup.
But I want to style the popup. But how to do that?
SO I have this template:
<mgl-layer
*ngIf="imageLoaded"
id="camera"
type="symbol"
[source]="{
type: 'geojson',
data: {
type: 'FeatureCollection',
}
}"
(click)= "onClick($event)"
[layout]="{'icon-image': 'camera', 'icon-size': 0.25}"
>
</mgl-layer>
<mgl-popup *ngIf="selectedPoint" [feature]="selectedPoint">
<span [innerHTML]="selectedPoint.properties?.description"></span>
</mgl-popup>
and ts:
allWifiPoints = this.wifiPoints.map((wifi) => ({
type: 'Feature',
properties: {
description:
// eslint-disable-next-line max-len
},
geometry: {
type: 'Point',
coordinates: wifi,
},
}));
onClick(evt: MapLayerMouseEvent) {
this.selectedPoint = evt.features![0];
}
and css:
.mapboxgl-popup-content-wrapper {
width: 89px;
}
but nothing change. The popup stays white
see image.
So what I have to change?
Thank you
So in css: toggle-layer.component.scss
I have this:
:host ::ng-deep .mapboxgl-popup-content-wrapper {
width: 89px;
}
Should work:
:host ::ng-deep .mapboxgl-popup-content-wrapper {
width: 89px;
height: max-content;
border: 2px solid #BF0404;
background-color: rgba(243, 207, 207, 0.7);
border-radius: 18px;
margin-bottom: 3px;
}
In Vue, what should I do when I want to click a component to add styles to this component and to clean up the additional styles that have been clicked on other tabbox components before? Thanks in advance!
Here is the code of the sub component
<template>
<div :class="tabStyle" :style="boxstyle" #click="tabClick(name)">
{{name}}
<div class="selected-icon" v-show="isSelected"></div> <!--selected styles-->
<div class="tick" v-show="isSelected"></div> <!--selected styles-->
</div>
</template>
<script>
export default {
name: "tabbox",
props: {
name: {
type: String,
default: ""
},
boxstyle: {
type: Object,
defalult: {}
}
},
data() {
return {
isSelected: false,
tabStyle: {
"selected-box": false,
"unselected-box": true
}
};
},
methods: {
tabClick(name) {
this.isSelected = true;
this.borderChange("selected-box","unselected-box")//style add
this.$emit("getTabName", name);
},
borderChange(first, second) {
this.tabStyle[first] = true;
this.tabStyle[second] = false;
}
}
};
</script>
<style lang="scss" scoped>
.tab-box {
display: inline-block;
position: relative;
text-align: center;
padding: 1%;
font-size: 1rem;
width: 20%;
}
.unselected-box {
border: solid 1px #b9a7a76b;
#extend .tab-box;
}
.selected-box {
border: solid 1px #5ddb14;
#extend .tab-box;
}
.selected-icon {
position: absolute;
right: 0;
bottom: 0;
width: 0;
height: 0;
border-color: #5ddb14 transparent;
border-width: 0 0 20px 25px;
border-style: solid;
}
.tick {
position: absolute;
right: 0;
bottom: 0;
color: #fff;
&::after {
content: "✓";
}
}
</style>
And This is the code of the parent component
<template>
<div class="select-tab" :style="tabStyle">
<Header></Header>
<div class="label-content" v-for="(item,index) in categories" :key="index">
<meaning-label :name="item.name"></meaning-label>
<div class="box-content">
<TabBox #getTabName="getTabName" :name="_item.name" :boxstyle="styles" v-for="(_item,_index) in item.categoryList" :key="_index">
</TabBox>
</div>
</div>
</div>
</template>
<script>
import TabBox from "#/components/FindMusic/SelectTab/TabBox";
import MeaningLabel from "#/components/FindMusic/SelectTab/MeaningLabel";
import Header from "#/components/FindMusic/SelectTab/Header";
export default {
components: {
TabBox,
MeaningLabel,
Header
},
methods: {},
data() {
return {
styles: {
width: ""
},
allStyles: {
width: "94%",
margin: "2px 1.5%"
},
_categories: {}
};
},
mounted() {
this.categories = this.$store.state.CategoriesInfo.categories;
},
props: {
tabStyle: {
type: Object,
default: {}
},
categories: {
type:Array,
default: []
}
},
methods: {
getTabName(name){
this.$emit('getTabName',name)
}
}
};
</script>
<style lang="scss" scoped>
.box-content {
display: inline-block;
vertical-align: top;
width: 75%;
font-size: 0;
}
.label-content {
margin-top: 10px;
}
</style>
Just keep the style on the tab i click right now and remove the style i has clicked before .
One possible solution is get boxstyle from a Vue method:
<template>
<div class="select-tab" :style="tabStyle">
<Header></Header>
<div class="label-content" v-for="(item,index) in categories" :key="index">
<meaning-label :name="item.name"></meaning-label>
<div class="box-content">
<TabBox #getTabName="getTabName" :name="_item.name" :boxstyle="getTableStyle(_item.name)" v-for="(_item,_index) in item.categoryList" :key="_index">
</TabBox>
</div>
</div>
</div>
</template>
<script>
import TabBox from "#/components/FindMusic/SelectTab/TabBox";
import MeaningLabel from "#/components/FindMusic/SelectTab/MeaningLabel";
import Header from "#/components/FindMusic/SelectTab/Header";
export default {
components: {
TabBox,
MeaningLabel,
Header
},
methods: {},
data() {
return {
styles: {
width: ""
},
allStyles: {
width: "94%",
margin: "2px 1.5%"
},
_categories: {},
activeTabName: ''
};
},
mounted() {
this.categories = this.$store.state.CategoriesInfo.categories;
// you might want to set default activeTabName here or in Vue's watch
},
props: {
tabStyle: {
type: Object,
default: {}
},
categories: {
type:Array,
default: []
}
},
methods: {
getTabName(name){
this.$emit('getTabName',name)
this.activeTabName = name
},
getTableStyle (name) {
if (name === this.activeTabName) {
return this.allStyles
}
return {}
}
}
};
</script>
<style lang="scss" scoped>
.box-content {
display: inline-block;
vertical-align: top;
width: 75%;
font-size: 0;
}
.label-content {
margin-top: 10px;
}
</style>