8 Commits

18 changed files with 586 additions and 375 deletions

View File

@@ -1,5 +1,15 @@
# Changelog # Changelog
## v0.1.5
[compare changes](https://gitea.websimple.com/templates/wp-headless/compare/v0.1.4...v0.1.5)
### 🩹 Fixes
- Auth server utils upgrade to latest nuxt-graphql (fd61895)
- Immutable extractNodes (baa3061)
- Type issue with NodePage (3b706c0)
## v0.1.4 ## v0.1.4
[compare changes](https://gitea.websimple.com/templates/wp-headless/compare/v0.1.2...v0.1.4) [compare changes](https://gitea.websimple.com/templates/wp-headless/compare/v0.1.2...v0.1.4)

View File

@@ -1,3 +1,12 @@
# Moonshine # Moonshine
Headless WordPress theme based on Nuxt. Thème WordPress en headless basé sur Nuxt.
## Varaibles d'environnement
| Nom | Description | Exemple | Requise |
|-----|-------------|---------|---------|
| `NUXT_SESSION_PASSWORD` | Clé secrète pour l'authentification | `date \| md5sum` | ✅ |
| `NUXT_WP_URL` | URL du backend WordPress | https://wp.exemple.com | ✅ |
| `NUXT_SITE_URL` | URL du frontend Nuxt | https://www.example.com | |
| `NUXT_SITE_ENV` | Environnement | staging \| production | |

View File

@@ -0,0 +1,60 @@
{
"key": "group_options_site",
"title": "Options - Site",
"fields": [
{
"key": "field_697220310aaaf",
"label": "Email",
"name": "email",
"aria-label": "",
"type": "email",
"instructions": "",
"required": 1,
"conditional_logic": 0,
"wrapper": {
"width": "",
"class": "",
"id": ""
},
"default_value": "",
"allow_in_bindings": 0,
"placeholder": "",
"prepend": "",
"append": "",
"show_in_graphql": 1,
"graphql_description": "",
"graphql_field_name": "email",
"graphql_non_null": 1
}
],
"location": [
[
{
"param": "options_page",
"operator": "==",
"value": "site-options"
}
]
],
"menu_order": 0,
"position": "normal",
"style": "seamless",
"label_placement": "top",
"instruction_placement": "label",
"hide_on_screen": "",
"active": true,
"description": "",
"show_in_rest": 0,
"display_title": "",
"acfe_autosync": [
"json"
],
"acfe_form": 0,
"show_in_graphql": 1,
"graphql_field_name": "GroupSite",
"map_graphql_types_from_location_rules": 0,
"graphql_types": "",
"acfe_meta": "",
"acfe_note": "",
"modified": 1769087407
}

View File

@@ -0,0 +1,25 @@
{
"key": "ui_options_page_site",
"title": "Options du site",
"active": true,
"menu_order": 0,
"page_title": "Options du site",
"menu_slug": "site-options",
"parent_slug": "options-general.php",
"advanced_configuration": 1,
"icon_url": "",
"menu_title": "",
"position": "",
"redirect": false,
"description": "",
"menu_icon": [],
"update_button": "Mise à jour",
"updated_message": "Options mises à jours",
"capability": "edit_posts",
"data_storage": "options",
"post_id": "",
"autoload": 0,
"show_in_graphql": 1,
"graphql_type_name": "OptionsSite",
"modified": 1769086997
}

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { BuilderSectionsFragment } from "#graphql/fragments"; import type { BuilderSectionsFragment } from "#graphql/operations";
const props = defineProps<BuilderSectionsFragment>(); const props = defineProps<BuilderSectionsFragment>();
const sections = computed(() => { const sections = computed(() => {

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { LayoutContainedFragment } from "#graphql/fragments"; import type { LayoutContainedFragment } from "#graphql/operations";
import { tv, type VariantProps } from "tailwind-variants"; import { tv, type VariantProps } from "tailwind-variants";
const props = defineProps<LayoutContainedFragment>(); const props = defineProps<LayoutContainedFragment>();

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { NodePageFragment } from "#graphql/fragments"; import type { NodePageFragment } from "#graphql/operations";
defineProps<NodePageFragment>(); defineProps<NodePageFragment>();
</script> </script>
@@ -9,6 +9,6 @@ defineProps<NodePageFragment>();
<h1 v-if="!isFrontPage" class="font-bold text-4xl"> <h1 v-if="!isFrontPage" class="font-bold text-4xl">
{{ title }} {{ title }}
</h1> </h1>
<BuilderSections v-bind="groupPostPage" /> <BuilderSections :sections="groupPostPage?.sections || []" />
</div> </div>
</template> </template>

View File

@@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { SectionTextBlockFragment } from "#graphql/fragments"; import type { SectionTextBlockFragment } from "#graphql/operations";
defineProps<SectionTextBlockFragment>(); defineProps<SectionTextBlockFragment>();
</script> </script>

View File

@@ -13,7 +13,7 @@ login( input: { provider: PASSWORD, credentials: { username: $username, password
authToken authToken
refreshToken refreshToken
user { user {
...AuthUser ... AuthUser
} }
} }
} }

View File

@@ -1,6 +1,10 @@
fragment GeneralSettings on GeneralSettings {
title
description
}
query GeneralSettings { query GeneralSettings {
generalSettings { generalSettings {
title ... GeneralSettings
description
} }
} }

View File

@@ -0,0 +1,11 @@
fragment SiteOptions on GroupSite_Fields {
email
}
query OptionsSite {
optionsSite {
groupSite {
... SiteOptions
}
}
}

View File

@@ -4,4 +4,5 @@
require_once __DIR__ . '/includes/core/theme-setup.php'; require_once __DIR__ . '/includes/core/theme-setup.php';
// Vendors // Vendors
require_once __DIR__ . '/includes/vendors/acf.php';
require_once __DIR__ . '/includes/vendors/tinymce.php'; require_once __DIR__ . '/includes/vendors/tinymce.php';

View File

@@ -0,0 +1,15 @@
<?php
// Disable ACF / ACFE modules
add_filter( 'acf/settings/enable_post_types', '__return_false' );
add_action( 'acf/init', 'moonshine_acf_init' );
function moonshine_acf_init() {
acf_update_setting( 'acfe/modules/block_types', false );
acf_update_setting( 'acfe/modules/categories', false );
acf_update_setting( 'acfe/modules/forms', false );
acf_update_setting( 'acfe/modules/options', false );
acf_update_setting( 'acfe/modules/options_pages', false );
acf_update_setting( 'acfe/modules/post_types', false );
acf_update_setting( 'acfe/modules/taxonomies', false );
acf_update_setting( 'acfe/modules/templates', false );
}

View File

@@ -1,7 +1,7 @@
{ {
"name": "@lewebsimple/moonshine", "name": "@lewebsimple/moonshine",
"description": "Headless WordPress theme based on Nuxt.", "description": "Headless WordPress theme based on Nuxt.",
"version": "0.1.4", "version": "0.1.5",
"type": "module", "type": "module",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -17,7 +17,7 @@
}, },
"dependencies": { "dependencies": {
"@iconify-json/lucide": "^1.2.86", "@iconify-json/lucide": "^1.2.86",
"@lewebsimple/nuxt-graphql": "^0.5.2", "@lewebsimple/nuxt-graphql": "^0.5.11",
"@nuxt/ui": "4.3.0", "@nuxt/ui": "4.3.0",
"@nuxtjs/seo": "^3.3.0", "@nuxtjs/seo": "^3.3.0",
"jwt-decode": "^4.0.0", "jwt-decode": "^4.0.0",

File diff suppressed because it is too large Load Diff

View File

@@ -51,6 +51,21 @@ interface AcfFieldGroupFields {
fieldGroupName: String @deprecated(reason: "Use __typename instead") fieldGroupName: String @deprecated(reason: "Use __typename instead")
} }
"""Options Page registered by ACF"""
interface AcfOptionsPage implements Node {
"""The globally unique ID for the object"""
id: ID!
""""""
menuTitle: String
""""""
pageTitle: String
""""""
parentId: String
}
"""The Headless Login authentication data.""" """The Headless Login authentication data."""
type AuthenticationData { type AuthenticationData {
"""A new authentication token to use in future requests.""" """A new authentication token to use in future requests."""
@@ -3480,6 +3495,32 @@ interface GroupPostPage_Fields implements AcfFieldGroup & AcfFieldGroupFields &
sections: [GroupAbstractBuilderSections_Layout] sections: [GroupAbstractBuilderSections_Layout]
} }
"""
The &quot;GroupSite&quot; Field Group. Added to the Schema by &quot;WPGraphQL for ACF&quot;.
"""
type GroupSite implements AcfFieldGroup & AcfFieldGroupFields & GroupSite_Fields {
"""
Field of the &quot;email&quot; Field Type added to the schema as part of the &quot;GroupSite&quot; Field Group
"""
email: String!
"""The name of the field group"""
fieldGroupName: String @deprecated(reason: "Use __typename instead")
}
"""
Interface representing fields of the ACF &quot;GroupSite&quot; Field Group
"""
interface GroupSite_Fields implements AcfFieldGroup & AcfFieldGroupFields {
"""
Field of the &quot;email&quot; Field Type added to the schema as part of the &quot;GroupSite&quot; Field Group
"""
email: String!
"""The name of the field group"""
fieldGroupName: String @deprecated(reason: "Use __typename instead")
}
""" """
Content that can be organized in a parent-child structure. Provides fields for navigating up and down the hierarchy and maintaining structured relationships. Content that can be organized in a parent-child structure. Provides fields for navigating up and down the hierarchy and maintaining structured relationships.
""" """
@@ -5926,6 +5967,23 @@ interface OneToOneConnection implements Edge {
node: Node! node: Node!
} }
type OptionsSite implements AcfOptionsPage & Node & WithAcfGroupSite {
"""Fields of the GroupSite ACF Field Group"""
groupSite: GroupSite
"""The globally unique ID for the object"""
id: ID!
""""""
menuTitle: String
""""""
pageTitle: String
""""""
parentId: String
}
""" """
Sort direction for ordered results. Determines whether items are returned in ascending or descending order. Sort direction for ordered results. Determines whether items are returned in ascending or descending order.
""" """
@@ -8436,7 +8494,7 @@ interface Previewable {
} }
"""The root entry point into the Graph""" """The root entry point into the Graph"""
type Query { type Query implements WithAcfOptionsPageOptionsSite {
"""Entry point to get all settings for the site""" """Entry point to get all settings for the site"""
allSettings: Settings allSettings: Settings
@@ -8721,6 +8779,9 @@ type Query {
uri: String! uri: String!
): UniformResourceIdentifiable ): UniformResourceIdentifiable
""""""
optionsSite: OptionsSite
"""An object of the page Type. """ """An object of the page Type. """
page( page(
""" """
@@ -14080,6 +14141,20 @@ interface WithAcfGroupPostPage {
groupPostPage: GroupPostPage groupPostPage: GroupPostPage
} }
"""
Provides access to fields of the &quot;GroupSite&quot; ACF Field Group via the &quot;groupSite&quot; field
"""
interface WithAcfGroupSite {
"""Fields of the GroupSite ACF Field Group"""
groupSite: GroupSite
}
"""Access point for the &quot;OptionsSite&quot; ACF Options Page"""
interface WithAcfOptionsPageOptionsSite {
""""""
optionsSite: OptionsSite
}
"""The writing setting type""" """The writing setting type"""
type WritingSettings { type WritingSettings {
"""Catégorie darticle par défaut.""" """Catégorie darticle par défaut."""

View File

@@ -2,15 +2,15 @@ import type { H3Event } from "h3";
import { GraphQLClient } from "graphql-request"; import { GraphQLClient } from "graphql-request";
import { jwtDecode } from "jwt-decode"; import { jwtDecode } from "jwt-decode";
import type { User } from "#auth-utils"; import type { User } from "#auth-utils";
import type { AuthUserFragment } from "#graphql/fragments"; import type { AuthUserFragment, AuthLoginMutationResult } from "#graphql/operations";
import { AuthRefreshTokenDocument, type AuthLoginResult } from "#graphql/operations"; import { AuthRefreshTokenDocument } from "#graphql/operations";
// Handle login result and store user session // Handle login result and store user session
export async function handleLogin(event: H3Event, loginData: AuthLoginResult) { export async function handleLogin(event: H3Event, loginResult: AuthLoginMutationResult) {
if (!loginData?.login) { if (!loginResult?.login) {
return false; return false;
} }
const { user, authToken, refreshToken } = loginData.login; const { user, authToken, refreshToken } = loginResult.login;
if (!user || !authToken || !refreshToken) { if (!user || !authToken || !refreshToken) {
return false; return false;
} }

View File

@@ -1,4 +1,4 @@
// Helper: Extracts nodes from a GraphQL connection object, returning an empty array if nodes are absent. // Helper: Extracts nodes from a GraphQL connection object, returning an empty array if nodes are absent.
export function extractNodes<T>(connection: { nodes?: T[] } | null | undefined): T[] { export function extractNodes<T>(connection: { nodes?: readonly T[] } | null | undefined): readonly T[] {
return connection?.nodes || [] as T[]; return connection?.nodes ?? [];
} }