Selected state on nav item in Next 13 - next.js

I want to have a selected state on a nav item in next 13, I could find no way of getting any context on a server component so ended up with a client component like this
'use client';
import Image from 'next/image';
import Styles from '../styles/header.module.css';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
interface MainRoute {
name: string;
path: string;
index: number;
}
const mainRoutes = [
{ name: 'home', path: '/' },
{ name: 'path 2', path: '/path2' },
{ name: 'path 3', path: '/path3' },
] as MainRoute[];
export default function Header({}) {
const path = usePathname();
return (
<header>
<div className={Styles.header}>
<h1>
App title
</h1>
</div>
<div className={Styles.header}>
<ul id="mainNav">
{mainRoutes.map((route, index) => (
<li key={index}>
<Link
className={path === route.path ? Styles.selected : ''}
href={route.path}
>
{route.name}
</Link>{' '}
{index !== mainRoutes.length - 1 ? <span>|</span> : ''}
</li>
))}
</ul>
</div>
</header>
);
}
Is this the best way to achieve this basic styling?
As far as I am aware, now all of this code has to be shipped over to the client.

Related

Vue Router link Ana Router exact in children

there are two components Dashboard and Apps and they have children. What I want to do is, when clicking any of the Main Components
showMenu is showing but,
I don't want the main components to be opened. and I want the main components and active class to be written
I want it to act as Dashboard and Appsself
app.vue
<template>
<router-link :to="{name:'dashboard'}" #click="isShow('dashboard')">Dashboard</router-link>
<router-link :to="{name:'apps'}" #click="isShow('apps')">Apps</router-link>
<div class="children">
<div class="dashboard" v-show="showName ==='dashboard' && showMenu">
<router-link exact :to="{name:'home'}">Home</router-link>
<router-link exact :to="{name:'about'}">About</router-link>
</div>
<div class="apps" v-show="showName ==='apps' && showMenu">
<router-link exact :to="{name:'setting'}">Setting</router-link>
<router-link exact :to="{name:'profile'}">Profile</router-link>
</div>
</div>
<div class="container">
<router-view/>
</div>
</template>
<script setup>
const showName = ref('')
const showMenu = ref(false)
function isShow(name) {
if (showName.value !== name) {
showMenu.value = true
}
if (showName.value === name) {
showMenu.value = !showMenu.value
} else {
showName.value = name
}
}
</script>
router.js
import {createRouter, createWebHistory, RouterView} from 'vue-router'
const routes = [
{
path: '/dashboards',
name: 'dashboards',
component: RouterView,
children: [
{
path: 'home',
component: () => import('../Views/home.vue'),
name: 'home',
},
{
path: 'about',
component: () => import('../Views/about.vue'),
name: 'about',
},
]
},
{
path: '/apps',
name: 'apps',
component: RouterView,
children: [
{
path: 'setting',
component: () => import('../Views/setting.vue'),
name: 'setting',
},
{
path: 'profile',
component: () => import('../Views/profile.vue'),
name: 'profile',
},
]
},
];
const router = createRouter({
linkActiveClass: "item-active",
linkExactActiveClass: "exact-active",
history: createWebHistory(),
routes
})
export default router;

Passing information into sub component in storybook

Is there a way to pass information down though components in storybook that do not inheartly pass props though.
A proacticle example of this being that i have a language select button (usually pulls its information from a store so no information is passed to it), for storybook i have some props avaiable for this so i can run tests etc without having dirrect access to the store. this component on its own works fine.
On top of this i have a header nav bar that utlises this component, but does not pass informaton to it (as its self managed), however when trying to now set this component up in storybook the language select does not display as not having data.
I was reading though the multi-component documentation but struggling to get this to work for this use case and begining to wonder if this is going to be possiable and if so whats the best way to approach this.
in terms of code this is what i currently have
//header.vue
<template>
<div class="HeaderComponent">
//other component code here
<language-selector/>
</div>
</div>
</nav>
</div>
</template>
<script setup lang="ts">
import LanguageSelector from "#/components/HeaderAndFooter/LanguageSelector.vue";
import _ from "underscore";
import ImageModel from "#/helpers/classes/core/ImageModel";
import { defineProps } from "vue";
import HeaderNavigation from "#/helpers/classes/Navigation/HeaderNavigation";
// eslint-disable-next-line no-unused-vars
const props = defineProps({
menu: {
type: Object as () => {[key: string]: HeaderNavigation} | undefined,
default: undefined
},
icon: {
type: Object as () => ImageModel | undefined,
default: undefined
}
})
</script>
///languageSelector.vue
<template>
<div class="languageSelector" v-if="props.languages.length > 1">
<div class="dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" id="languageDropdownButton" data-bs-toggle="dropdown" aria-expanded="false">
{{currentLang.name}}
</button>
<ul class="dropdown-menu" aria-labelledby="languageDropdownButton">
<li v-for="language in props.languages" :key="language.i18n">
<a class="dropdown-item" href="#" #click="ChangeLanguage(language.i18n)">{{language.name}}</a>
</li>
</ul>
</div>
</div>
</template>
<script setup lang="ts">
import {useLanguageStore} from "#/store/languageStore";
import {computed, defineProps} from "vue";
import Language from "#/helpers/classes/Language";
const props = defineProps({
languages: {
languages: Object as () => Language[],
default: useLanguageStore().availableLanguages
},
})
const currentLang = computed(() => useLanguageStore().currentLanguage);
function ChangeLanguage(i18nCode : string)
{
useLanguageStore().updateCurrentLanguage(i18nCode);
}
</script>
///header.story.js
import LanguageSelector from "#/components/HeaderAndFooter/LanguageSelector.vue";
import { DefaultLangSelector } from './LanguageSelector.stories.js';
// More on default export: https://storybook.js.org/docs/vue/writing-stories/introduction#default-export
export default {
title: 'Framework/Header',
component: HeaderComponent,
// More on argTypes: https://storybook.js.org/docs/vue/api/argtypes
argTypes: {},
};
// More on component templates: https://storybook.js.org/docs/vue/writing-stories/introduction#using-args
const Template = (args) => ({
components: { HeaderComponent, LanguageSelector },
setup() {
return { args };
},
template: '<header-component v-bind="args" />',
});
export const Primary = Template.bind({});
Primary.args = {
menu: {
"Item 1": {
"children": {},
"id": "1",
"title": "Item 1"
},
"Item 2": {
"children": {},
"id": "2",
"title": "Item 2"
}
},
icon:
{
"url": "https://th.bing.com/th/id/R.25df89f6bbce9ce2324fb746d01940ea?rik=WpLZgCWrgPXbLA&pid=ImgRaw&r=0"
},
...DefaultLangSelector.args = {
languages: [
{"name" : "English", "i18n" : "en"},
{"name" : "Spanish", "i18n" : "es"},
],
}
};

How can I use a Map with pinia/vue3

I am trying to set values in a map stored in a pinia store (persisted into localstorage)
in this minimal (non) working example pushing elements into a list works fine but adding a element into a Map does not persist.
component.vue
<script setup lang="ts">
import { useStore } from "./store"
const store = useStore()
</script>
<template>
<main>
<button
#click="store.createA"
>
Create new Array item
</button>
<ul v-if="store.array.length > 0">
<li v-for="(item) in store.array">
{{ item }}
</li>
</ul><br/>
<button
#click="store.createO"
>
Create Map Object
</button>
<ul v-if="store.mapObj.size > 0">
<li v-for="(item) in store.mapObj">
{{ item[1] }}
</li>
</ul>
</main>
</template>
store.ts
import { defineStore } from "pinia"
export const useStore = defineStore({
id: "character-manager",
state: () => ({ array: [] as Array<{key: string, value: string }>,
mapObj: new Map() as Map<string,string>}),
actions: {
createA() {
this.array.push({key: "key", value: "value"})
},
createO() {
this.mapObj.set("key", "value")
},
},
persist: {
enabled: true,
strategies: [{ storage: localStorage }],
},
})

Storybook Navbar component breaks through useRouter()

I have a Navbar with a Navigation Links, which are highlighted as active, depending on the page a user is on.
import { useRouter } from 'next/dist/client/router'
const navigation = [
{ name: 'Home', url: '/', active: true},
{ name: 'Quiz', url:'/quiz', active: false}
]
function classNames(...classes) {
return classes.filter(Boolean).join(' ')
}
export const Navigation = () => {
const router = useRouter();
return (
<div className="flex space-x-4 justify-self-center">
{navigation.map((item) => (
<a
key={item.name}
href={item.url}
className={`px-3 py-2 rounded-md text-sm font-medium ${
router.asPath === item.url ? "bg-gray-900 text-white" : 'text-gray-300 hover:text-gray-700'
}`}
aria-current={item.active? 'page' : undefined}
>
{item.name}
</a>
))}
</div>
)
}
It works as is should, but I can't create a story for it, because of the Next Router. I get the following error: Cannot read property 'asPath' of null.
I tried to follow the instructions in the answer I found here: How to use NextJS's 'useRouter()' hook inside Storybook , but unfortunately it didn't give me the result I am striving for. So basically my "Story-Navbar" shouldn't redirect, but just highlight the Navigation Link, while I click on it. Is that possible? Here is the story for Navigation:
import { Navigation } from './Navigation';
export default {
title: 'Example/Navigation',
component: Navigation,
};
export const DefautlNavigation = () => {
return (
<Navigation />
)
}
The storybook Next.js router add-on works for useRouter() hook. Here is the example in their docs:
import { withNextRouter } from 'storybook-addon-next-router';
import MyComponentThatHasANextLink from '../component-that-has-a-next-link';
export default {
title: 'My Story',
decorators: [withNextRouter], // not necessary if defined in preview.js
};
// if you have the actions addon
// you can click the links and see the route change events there
export const Example = () => <MyComponentThatHasANextLink />;
Example.story = {
parameters: {
nextRouter: {
path: '/profile/[id]',
asPath: '/profile/lifeiscontent',
query: {
id: 'lifeiscontent',
},
},
},
};

Gutenberg block fails validation

I'm trying to create a custom WordPress gutenberg block that allows me to select any image and text.
I can create the block using this code and I can see content on the frontend of the site however the block crashes if I view the block to edit it.
The console gives the following error: -
Block validation: Block validation failed for cgb/block-imagecta (
Object { name: "cgb/block-imagecta", icon: {…}, attributes: {…}, keywords: (3) […], save: save(), title: "imagecta - CGB Block", category: "common", edit: edit()
}
).
Content generated by save function:
<div class="wp-block-cgb-block-imagecta"><div class="imageCtaBg"><img class="bg" alt="sergserge"/><p></p></div></div>
Content retrieved from post body:
<div class="wp-block-cgb-block-imagecta"><div class="imageCtaBg"><img class="bg" alt="sergserge"/><p>dxfbxdfbxdfbfb</p></div></div>
Is this related to the attributes at all?
/**
* BLOCK: imagecta
*
* Registering a basic block with Gutenberg.
* Simple block, renders and saves the same content without any interactivity.
*/
// Import CSS.
import './editor.scss';
import './style.scss';
const {__} = wp.i18n; // Import __() from wp.i18n
const {registerBlockType} = wp.blocks; // Import registerBlockType() from wp.blocks
const {FormFileUpload} = wp.components;
const {RichText, MediaUpload} = wp.blockEditor;
registerBlockType('cgb/block-imagecta', {
title: __('imagecta - CGB Block'),
icon: 'shield',
category: 'common',
keywords: [
__('imagecta — CGB Block'),
__('CGB Example'),
__('create-guten-block'),
],
attributes: {
content: {
type: 'html',
selector: '.captionText',
},
logo: {
type: 'string',
default: null,
},
},
edit: (props) => {
const {attributes: {content}, setAttributes} = props;
function onImageSelect(imageObject) {
setAttributes({
logo: imageObject.sizes.full.url
});
}
const onChangeContent = (newContent) => {
setAttributes({content: newContent});
};
// Creates a <p class='wp-block-cgb-block-imagecta'></p>.
return (
<div className={props.className}>
<MediaUpload
onSelect={onImageSelect}
type="image"
value={logo} // make sure you destructured backgroundImage from props.attributes!
render={({open}) => (
<button onClick={open}>
Upload Image!
</button>
)}
/>
<div className='imageCtaBg'>
<img src={logo} class="bg" alt={'sergserge'}/>
<RichText
tagName={'p'}
className='captionText'
onChange={onChangeContent}
value={content}
placeholder='Enter text...'
/>
</div>
</div>
);
},
save: (props) => {
const {attributes: {content}, setAttributes} = props;
return (
<div className={props.className}>
<div className={'imageCtaBg'}>
<img src={props.attributes.logo} className="bg" alt={'sergserge'}/>
<RichText.Content tagName={"p"} value={props.attributes.content}/>
</div>
</div>
);
},
});
The issue was to do with the content attribute.
Instead of giving the richtext the class of 'captionText' I moved it do a wrapping div and changed the content attribute to the following: -
content: {
type: 'string',
source:'html',
selector: '.captionText',
},
This finds the text inside of that div and allows me to update the block with no validation issues.

Resources